ここまでで、メソッドに手続きそのものを渡す方法としてのブロックを紹介しました。実は、「Proc」クラスや「lambda(ラムダ)」という仕組みを使うことで、ブロックとして書いた手続きそのものをオブジェクト化して引き回す方法があります。block03.rbに、その例を示しましょう。
procedure1 = Proc.new { |word| puts word } procedure2 = proc { |word| puts word } procedure3 = lambda { |word| puts word } procedure4 = ->(word) { puts word } procedure5 = -> { puts "cluck" } procedure1.call("meow") procedure2.call("quack") procedure3.call("oink") procedure4.call("bowwow") procedure5.call
$ ruby block03.rb meow quack oink bowwow cluck
1行目はProc.newを使う方法で、2行目はprocメソッドを使う方法、3行目はlambdaメソッドを使う方法です。Proc.newはprocと比較して、よりシンプルに書けるという以外の違いはありません。4・5行目は「lambda」メソッドの別の書き方で、引数のカッコは省略もできます。どの記法を利用してもProcクラスのオブジェクトが生成されるので、そのまま変数に代入できます。
これらの方法で作成したProcオブジェクトは、callメソッドによって7〜11行目のように呼び出せます。callメソッドに与えた引数が、そのままブロック引数に対応します。
メソッドに与えるブロックとProcオブジェクトの関係を理解するために、メソッドに与えたブロックをProcオブジェクトとして利用したり、Procオブジェクトをブロックとして与えたりする例をblock04.rbに示します。
def repeat1(n) for i in 1..n puts "#{i} =>" yield end end def repeat2(n, &procedure) for i in 1..n puts "#{i} =>" procedure.call end end procedure = -> { puts "quack" } repeat1(3, &procedure) repeat2(3) { puts "meow" }
$ ruby block04.rb 1 => quack 2 => quack 3 => quack 1 => meow 2 => meow 3 => meow
repeat1メソッドはblock01.rbにおけるrepeatと全く同じです。repeat2メソッドは、引数として「&(アンパサンド)」から始まる「procedure」を用意しています。このように、「&」から始まる引数を定義すると、メソッドに与えられたブロックをProcオブジェクトに変換したものを変数として受け取ることができます。
また、15行目で変数「procedure」にProcオブジェクトを代入し、「repeat1」の引数として「&」を付加して利用しています。このように記述すると、メソッドに与えたProcオブジェクトを、メソッドの定義元でyieldを用いて呼び出せるようになります。
Proc.newもlambdaメソッドもProcオブジェクトを生成しますが、returnの挙動に違いが生じます。block05.rbにその違いを示します。
def hoge procedure = Proc.new { return "return from Proc.new" } procedure.call return "return from hoge" end def fuga procedure = -> { return "return from lambda" } procedure.call return "return from fuga" end puts hoge puts fuga
$ ruby block05.rb return from Proc.new return from fuga
hogeはProc.newによって生成したProcオブジェクトの中でreturnを記述した場合で、fugaはlambdaによるProcオブジェクトをの中でreturnを記述した場合の例になります。
実行結果より、Proc.newの場合はreturnに出会った瞬間に、Procオブジェクトが生成されたメソッドを抜けるという動作になっていることが分かります。対するlambdaの場合は、returnが実行されると単にブロックを抜け、以下に続く処理が実行されていることが分かります。
つまり、lambdaによって生成されたProcオブジェクトは、Proc.newによって生成されたProcオブジェクトよりもメソッドに近い動作を行うといえます。
ここまで紹介したように、Procオブジェクトを生成するためには、さまざまなやり方が存在します。ですので、procとlambda、どちらを使えばよいのか迷うことになります。それらの違いが問題とならない場合はどちらでも良いのですが、プロジェクトの慣例に倣うのが無難でしょう。
筆者の場合、もし自分で選べる状況であれば、よりスマートに書けるlambdaの「->」記法を使うようにしています。そもそも、lambdaが生まれた背景には、ErlangやHaskellなどの関数型言語の「ラムダ式」という仕組みがあります。それらの言語でも、ラムダ式を記述する場合に記号として「->」を用います。lambdaの出自が関数型言語なので、それに倣うのは自然なことでしょう。
Rubyは「クロージャ」という概念を、Procオブジェクトによって実現しています。クロージャとは、ある手続きの引数に指定した変数以外の変数を、その手続きが定義されたコンテキストによって解決する仕組みを指します。
言葉で説明すると、どうしても抽象的になって分かりづらいので、block06.rbにクロージャの性質を示す例を挙げてみます。
def increase n = 0 -> { n += 5 } end 3.times { p increase.call } puts "==================" increase_proc = increase 3.times { p increase_proc.call }
$ ruby block06.rb 5 5 5 ================== 5 10 15
これを初めて見る方は、頭が混乱するかもしれません。ややこしいので一つずつ解きほぐしていきましょう。
まず、1〜4行目で、「increase」メソッドを定義しています。increaseメソッドは、まず変数「n」を0に初期化して、そのnを使ってn自身に5を足し込むProcオブジェクトを生成して呼び出し元に返しています。
6行目の実行では3回とも5が出力されており、11行目の実行では実行されるたびに5が足し込まれています。この違いは、6行目ではincreaseメソッドが実行されるたびにProcオブジェクトが生成され、11行目の実行では、10行目に生成したProcオブジェクトを再利用していることによって起こります。
Procオブジェクトを紹介したときに、「これは手続きそのものをオブジェクト化したものだ」と説明しました。しかしながら、本当のところは、「手続きと、その手続きが定義されたコンテキスト(環境)をオブジェクト化したもの」と言った方が正しいです。つまり、Procオブジェクトを生成するとき、その手続きのみならずコンテキストまで「包み込んで」オブジェクト化しているのです。
先ほどの動作の違いは、この「コンテキストを包み込んでいる」という性質によって説明できます。6行目の実行では実行のたびにProcオブジェクトが生成されているので、おのおののProcオブジェクトが持つコンテキストは別々のコンテキストとなるため5が3回出力されます。対する11行目の実行では、10行目で生成したProcオブジェクトを変数increase_procに代入し、3回callしています。
1回目の実行で、increase_procに包み込まれているコンテキストにおける変数nに5が足し込まれます。2回目の実行においても、同様にincrease_procに包み込まれているコンテキストにおける変数nが利用されるので、その値は、5に5を足し込んだ10となります。3回目も同様です。つまり、Procオブジェクトが同一であれば、そこに包み込まれているコンテキストも同一である、というわけです。このような変数のスコープを、「レキシカルスコープ」と言ったりもします。
クロージャの仕組みがあるおかげで、eachメソッドなどに与えるブロックの中で安心してローカル変数を使えます。
prefix = "@" ["a", "b", "c"].each do |s| puts prefix + s end
$ ruby block06.rb @a @b @c
block07.rbは、ブロックとeachメソッドを組み合わせた典型的な例です。変数prefixはブロックの外で定義されていますが、コンテキストを包み込むクロージャの仕組みのおかげで、eachメソッドの定義元でブロックを実行したとき、意図通りにprefixに「@」が代入されているというわけです。
連載第8回にあたる今回は、今まで何気なく使っていたメソッドやブロックについて掘り下げて解説しました。これらの知識は、クラスやモジュールの知識と合わせて、筆者がRubyで一番面白いと感じるメタプログラミングに通じる基盤となります。
次回は、メタプログラミングに入る前に、スレッドや入出力、例外処理など、残りのRubyの基本機能について解説します。お楽しみに!
麻田 優真(Rails技術者認定シルバー試験問題作成者)
イタリア、ローマ生まれ。中学生のころHSPに初めて触れ、プログラミングの楽しさを知る。オープンソースやハッカーカルチャーを好む。C#からRubyに転向したときに、動的型付け言語の柔軟性やメタプログラミングの魅力に感動し、Rubyとともにプログラマーとしての人生を歩む決意を固める。
現在は奈良先端科学技術大学院大学で学生として所属するかたわら、株式会社アジャイルウェアでプログラマーとして従事。Ruby on Railsによるコンシューマー向けのWebサービスの開発などに尽力している。
好きなメソッドは、define_method。
Twitter:@Mozamimy、ブログ:http://blog.quellencode.org/
山根 剛司(Ruby業務開発歴7年)
兵庫県生まれ。1997年からベンチャー系のパッケージベンダーで10年間勤務。当時、使用していた言語はJavaとサーバーサイドJavaScript。
2007年よりITコンサル会社に転職し、Rubyと出会って衝撃を受ける。基幹システムをRuby on Railsで置き換えるプロジェクトに従事。それ以来Ruby一筋で、Ruby on Railsのプラグインやgemも開発。
2013年より、株式会社アジャイルウェアに所属。アジャイルな手法で、Ruby on Railsを使って企業向けシステムを構築する業務に従事。
Ruby関西所属。
Twitter:@spring_kuma、Facebook:山根 剛司
Copyright © ITmedia, Inc. All Rights Reserved.