名古屋で数学するプログラマ(仮)

@antimon2 が趣味兼一部本職の数学で何かするときのブログ。

「yieldのお勉強 Lv.1」解説補足 #CodeIQ

この記事は、「yieldのお勉強 Lv.1」解説(Ruby編) および
「yieldのお勉強 Lv.1」解説(Python編)
の続きです(主に Ruby 編)。
前回紹介・解説しそびれたトピックについて追加で解説致します。

前回の記事を未読の方はそちらからお読みいただくことをオススメします。

Ruby 編別解(ただし不正解)

Ruby 編にて、こんな解答をお送りくださった方がいらっしゃいました(drop メソッドのみ抜粋):

  def drop n
    enum = to_enum.lazy.drop(n)
    return enum unless block_given?

    enum.each do |x|
      yield x
    end
  end

これは、Ruby2.0 から導入された Enumerable#lazy を用いた別解です。
さらに Enumerator::Lazy クラスでオーバーライドされた drop メソッドによる遅延リスト版の drop をそのまま利用した形になっています。

また、前回の解説記事公開後に、Twitterでコメントがあり、以下のようにさらに簡単に書くことが出来ます。

  def drop n, &block
    each.lazy.drop(n).each(&block)
  end

ブロック引数(説明後ほど)を利用し、「ブロックが与えられたら each で列挙が走り、与えられていなければ each が内部で to_enum を実行して Enumerator(正確には Enumerator::Lazy)が返る」という仕組みです。

今回の問題に「遅延」というキーワードを見いだしており、また .lazy というメソッドの存在を知っているからこそ思いつく解答であり、それは素晴らしいことだと思います(^-^)

ただし。
今回はこれは、不正解の扱いとなります。

理由は単純。
今回の対象はあくまで、Ruby 1.9.3以上 であり、Ruby 1.9.3 で動かないものはNGだから*1

それに、これは不正解の理由ではありませんが、出題側の意図として、

  • yield を利用するコードを書く練習をしてもらいたい。
  • 既存のメソッド drop を自分で実装することで、「どういうときに何を yield すれば良いか」等の感覚をつかんでもらいたい。

と言った思惑がありました。
知識のある方や中級者以上には、退屈な問題だったかもしれません。
ただ実際、「練習になった」「勉強になった」というお声もコメントでいただいているので、それは思惑通りでこちらとしても嬉しかったです(^-^)

Ruby 編「yield か、ブロック引数か?」

先ほど何気に「ブロック引数」というものを登場させてしまいましたが、実は Ruby で「ブロック付きメソッド*2」を定義する方法は2種類あります。

  • キーワード yield を利用する方法
  • ブロック引数を利用する方法

ブロック引数とは、メソッド宣言の引数の最後に指定する、「&〜」という書式の引数です。先ほどの例:

  def drop n, &block
    each.lazy.drop(n).each(&block)
  end

で言うと、「def drop n, &block」の「&block」が、ブロック引数です。

ブロック引数は、そのメソッドに渡されたブロックを Proc オブジェクトとして受け取れます。
Proc オブジェクトは call メソッドを持っており、これを呼び出すことで元のブロックを呼び出すことが出来ます。もちろん引数も渡せます。つまりこれが yield の代わりになります。
またオブジェクトなので、そのまま再利用も出来ます(インスタンス変数に保存、他のメソッドに受け渡し、等)。

ただし。今回はブロック引数については全く触れていません。
むしろ敢えて「必ず yield を使用すること」とそちらを強制してブロック引数を使わないように仕向けています。
これにも実は、理由があります。
その1つは「Python の yield との比較」があるのですが、その他にもちゃんと別の理由が。

yield とブロック引数、どちらもほぼ同じことが実現できるわけですが、実はそれぞれ、以下のような特長があります。

  • yield
  • ブロック引数
    • 「ブロックを呼び出している」ことが直感的になる(コードの可読性が高まる)。
    • 使い回し(インスタンス変数に保存、他のメソッドに受け渡し、等)ができる。

ここでいう「イテレータ」とは、前回記事の脚注で触れた「Ruby の用語としては正式名称ではなくなったブロック付きメソッドの別名」を指しているのではなく、もっと広く一般用語としての「イテレータ(=反復子)」です。
「内部イテレータ/外部イテレータ」と言う言葉についても詳しく述べたかったのですが、なんか記事が無駄に長くなりそうなので、簡単に。

Python の yield で実装される generator(iterator) は、外部イテレータです。
Ruby の yield で実装される ブロック付きメソッド は、内部イテレータです。ただし、ブロック省略時に to_enum で Enumerator に変換した場合、この Enumeratora オブジェクトは外部イテレータです。

とにかく、今回は、「ブロックの再利用は不要」、「繰り返し処理・列挙にターゲットを当てている」という理由で、「yield」の使い方、というテーマを根底に流している、というわけです。

とはいえ、イテレータ(反復子)を定義するのにブロック引数を使うことも、まぁ別に構わないと言えば構わないです。
実際、私も yield よりは ブロック引数 を好んで使います^_^; その最大の理由はやっぱり「可読性」。
でもそこを、グッとがまんして、今 yield に慣れておけば、将来視界が開けると思うのです。
今回の問題のように、自分でイテレータ(反復子)を書くことになったときに、yield を使って慣れておくことで、その列挙法の思考が養われると思うのです。

おしまいに

Python の話題は今ちょっと出てきただけでほぼ Ruby の話で終始してしまいました^_^; が、問題内容が同じなら、今回は Python の方が Ruby よりもできることが限定されている分、トピックも少なくなってしまうので仕方がありません。
ところで今回の Lv.1 問題、Python の方が挑戦者が多かったのですが、これは正直意外でした。Python に注目している人が多い、ということなのでしょうか。

なお、現在 Lv.2 問題公開中です。と言っても1週間遅れての公開でしたので、こちらも締切間近!来週の月曜朝までです!
こちらは、Ruby/Python で問題内容が別物。今度は Python の方が少し難しい問題になっています(と思います)。

さらに次のレベルの問題も準備中。最終調整はほぼ終わり、おそらく週明け、Lv.2問題と入れ替わりで公開される見込みです。
みなさんの挑戦、お待ちしております(^-^)

またこれからも、色々な問題を出題していくつもりです。
今後も、ただの知識問題ではなく、「標準機能だけどあまり使われていない?もの」にターゲットを当てた「お勉強問題」とか、とにかく自分で手を動かす「実用問題」とかを出して行けたらな、と思っております(^-^)
ご期待ください!*3

*1:Python編の方はバージョン申告を受け付けてバージョン依存の解も認めましたが、それは ideone.com に両バージョンが用意されているからと言うだけでなく、同じ記述をしようとしても上手に書かないと互換性のないコードになり得る(バージョン間の後方互換性がない)からです。Ruby は1.9以降は機能追加による非互換性はもちろんありますが仕様変更による非互換性は少なく、1.9 向けに書いたコードのほとんどは手直しなしで 2.0 以上でも動きます。

*2:ブロックを受け取るメソッドのこと。前回の記事も参照。

*3:自分でハードル上げたような気ガス