Rubyのクロージャ入門:ブロック、Proc、Lambdaの違いを徹底解説
Ruby Magicでは、日常的に使っている機能の裏側にある仕組みを掘り下げて理解することを大切にしています。今回のテーマは、ブロック(block)、Proc、ラムダ(lambda)の違いについてです。
第一級関数(ファーストクラス関数)を持つプログラミング言語では、関数を変数に格納したり、他の関数に引数として渡したりできます。さらに、関数が別の関数を戻り値として返すことさえ可能です。
クロージャとは、「環境」を持つ第一級関数のことです。環境とは、クロージャが生成された時点で存在していた変数へのマッピングを指します。クロージャは、たとえそれらの変数が別のスコープで定義されていても、その変数へのアクセスを保持し続けます。
Rubyには第一級関数はありませんが、ブロック・Proc・ラムダという形でクロージャを実現しています。ブロックはメソッドへコードのかたまりを渡すために使われ、Procとラムダはコードブロックを変数に保存することを可能にします。
ブロック
Rubyにおけるブロックは、後から実行できるように作成されるコードの断片です。ブロックは、doとendキーワードの間でyieldされるメソッドに渡されます。代表的な例の一つが、列挙可能なオブジェクトをループ処理する#eachメソッドです。
[1,2,3].each do |n|
puts "#{n}!"
end
[1,2,3].each { |n| puts "#{n}!" } # 1行で書いた同等のコード
この例では、ブロックがArray#eachメソッドに渡され、配列の各要素に対してブロックが実行され、その結果がコンソールに出力されます。
def each
i = 0
while i < size
yield at(i)
i += 1
end
end
これはArray#eachを簡略化した例です。whileループの中でyieldが呼び出され、配列の各要素に対して渡されたブロックが実行されます。このメソッドには引数がない点に注目してください。ブロックは暗黙的にメソッドへ渡されるためです。
暗黙的なブロックとyieldキーワード
Rubyでは、メソッドはブロックを暗黙的にも明示的にも受け取ることができます。暗黙的なブロック渡しは、メソッド内でyieldキーワードを呼び出すことで実現します。yieldキーワードは特別な存在です。渡されたブロックを見つけて呼び出してくれるため、メソッドの引数リストにブロックを追加する必要がありません。
Rubyは暗黙的なブロック渡しを許しているため、すべてのメソッドをブロック付きで呼び出すことができます。ただし、メソッドがyieldを呼び出さなければ、そのブロックは単に無視されます。
irb> "foo bar baz".split { p "block!" }
=> ["foo", "bar", "baz"]
一方、呼び出されたメソッドが実際にyieldを行う場合は、渡されたブロックが見つかり、yieldキーワードに渡された引数とともに呼び出されます。
def each
return to_enum(:each) unless block_given?
i = 0
while i < size
yield at(i)
i += 1
end
end
この例では、ブロックが渡されなかった場合にEnumeratorのインスタンスを返します。
yieldとblock_given?キーワードは、現在のスコープ内でブロックを探します。これによりブロックを暗黙的に渡せる一方、ブロックが変数に格納されないため、コードから直接ブロックへアクセスすることはできません。
明示的なブロック渡し
アンパサンド(&)パラメータ(慣習的に&blockと書かれます)を使えば、メソッドでブロックを明示的に受け取ることができます。ブロックが明示的になると、yieldに頼る代わりに、生成されたオブジェクトに対して直接#callメソッドを呼び出せます。
&block引数は通常の引数ではないため、ブロック以外のものを渡してこのメソッドを呼び出すとArgumentErrorが発生します。
def each_explicit(&block)
return to_enum(:each) unless block
i = 0
while i < size
block.call at(i)
i += 1
end
end
このようにブロックを渡して変数に格納すると、自動的にProcオブジェクトへ変換されます。
Proc
「Proc」とはProcクラスのインスタンスで、実行すべきコードブロックを保持し、変数に格納することができます。Procを作成するには、Proc.newを呼び出してブロックを渡します。
proc = Proc.new { |n| puts "#{n}!" }
Procは変数に格納できるため、通常の引数と同じようにメソッドへ渡すこともできます。その場合、Procは明示的に渡されるので、アンパサンドは不要です。
def run_proc_with_random_number(proc)
proc.call(random)
end
proc = Proc.new { |n| puts "#{n}!" }
run_proc_with_random_number(proc)
Procを作成してメソッドに渡す代わりに、先ほど見たRubyのアンパサンドパラメータ構文を使い、ブロックを渡すこともできます。
def run_proc_with_random_number(&proc)
proc.call(random)
end
run_proc_with_random_number { |n| puts "#{n}!" }
メソッドの引数にアンパサンドが付いている点に注目してください。これにより、渡されたブロックがProcオブジェクトに変換され、メソッドスコープ内の変数に格納されます。
ヒント: 場合によってはメソッド内でProcオブジェクトを持てることは便利ですが、ブロックからProcへの変換にはパフォーマンス上のコストがかかります。可能な限り、暗黙的なブロックを使用しましょう。
#to_proc
シンボル、ハッシュ、メソッドは、それぞれの#to_procメソッドによってProcへ変換できます。よく見かける使い方としては、シンボルから作ったProcをメソッドに渡すケースがあります。
[1,2,3].map(&:to_s)
[1,2,3].map {|i| i.to_s }
[1,2,3].map {|i| i.send(:to_s) }
この例は、配列の各要素に対して#to_sを呼び出す3つの等価な方法を示しています。1つ目では、アンパサンドを前置したシンボルが渡され、#to_procメソッドの呼び出しによって自動的にProcへ変換されます。後の2つは、そのProcが内部的にどのような動作をするかを表しています。
class Symbol
def to_proc
Proc.new { |i| i.send(self) }
end
end
簡略化した例ではありますが、Symbol#to_procの実装は内部で何が起きているかを示しています。このメソッドは、1つの引数を受け取り、そこへselfを送るProcを返します。このコンテキストでのselfはシンボル自身なので、結果としてInteger#to_sメソッドが呼び出されます。
ラムダ
ラムダは本質的にはProcですが、いくつかの際立った特徴があります。「通常の」メソッドに近い振る舞いをする点が2つあります。1つは、呼び出し時に引数の個数を厳密にチェックすること、もう1つは「通常の」returnを使うことです。
引数を期待するラムダを引数なしで呼び出したり、引数を取らないラムダに引数を渡したりすると、RubyはArgumentErrorを発生させます。
irb> lambda (a) { a }.call
ArgumentError: wrong number of arguments (given 0, expected 1)
from (irb):8:in `block in irb_binding'
from (irb):8
from /Users/jeff/.asdf/installs/ruby/2.3.0/bin/irb:11:in `<main>'
また、ラムダはreturnキーワードをメソッドと同じように扱います。Procを呼び出すとき、プログラムはProc内のコードブロックへ制御を委ねます。つまり、Procがreturnすれば、現在のスコープもreturnします。関数の中でProcが呼ばれてreturnを実行すると、その関数も即座にreturnします。
def return_from_proc
a = Proc.new { return 10 }.call
puts "This will never be printed."
end
この関数は制御をProcへ委ねるため、Procがreturnすると関数もreturnします。この例の関数を呼び出しても出力は表示されず、10が返されます。
def return_from_lambda
a = lambda { return 10 }.call
puts "The lambda returned #{a}, and this will be printed."
end
ラムダの場合は、この行が表示されます。ラムダ内でreturnを呼び出すのは、メソッド内でreturnを呼び出すのと同じように振る舞うため、変数aには10が代入され、その行がコンソールに出力されます。
ブロック、Proc、ラムダのまとめ
ここまでブロック、Proc、ラムダを詳しく見てきました。最後に、比較をまとめておさらいしましょう。
- ブロックは、Rubyでコードの断片をメソッドへ渡すために広く使われています。
yieldキーワードを使えば、ブロックをProcへ変換することなく暗黙的に渡せます。 - アンパサンドを付けたパラメータを使うと、メソッドにブロックを渡した時点で、メソッドのコンテキスト内ではProcとして扱われます。Procはブロックのように振る舞いますが、変数に格納できる点が異なります。
- ラムダはメソッドのように振る舞うProcです。つまり、引数の個数(アリティ)を強制し、親スコープではなく自分自身の中でreturnします。
以上で、Rubyにおけるクロージャの解説は終わりです。クロージャについては、レキシカルスコープやBindingなど、まだ学ぶべきトピックがありますが、それらは今後の回に取っておきましょう。それまでの間、Ruby Magicの次回のテーマ(クロージャ関連でもその他でも)についてのご要望があれば、@AppSignalまでぜひお知らせください。
-
Rubyのラムダ徹底解説――基本から実践的な応用テクニックまで
Rubyにおいてブロックは非常に重要な存在であり、ブロックなしにこの言語を語ることはできません。では、ラムダはどうでしょうか?ラムダを愛している人はいるのでしょうか?実際のところ、何年もラムダを使わずに過ごすことだって可能かもしれません。まるで昔の時代の遺物のように見えることもあるでしょう。 しかし、それは完全な真実ではありません。少し掘り下げてみると、ラムダには興味深い技が数多く隠されているのです。 この記事では、まずラムダの基本的な使い方から始め、その後、より高度で面白い応用へと進んでいきます。すでに毎日ラムダを使いこなしている方は、ぜひ後半の応用編までスクロールしてください。 ラムダにつ
-
Rubyでのログ出力をマスターする:LoggerとLogrageの使い方徹底解説
Rubyでのログ出力入門:LoggerとLogrageの使い方 ロギングは、アプリケーション開発において最も重要なタスクの一つです。ログは以下のような場面で活用されます。 アプリ内部で何が起きているかを把握したいとき アプリケーションを監視したいとき 特定のデータに関するメトリクスを収集したいとき 新しいプログラミング言語を学ぶ際、最初に選ばれるのはその言語がネイティブに備えているロギング機構でしょう。標準機能は通常、扱いやすく、ドキュメントも充実しており、コミュニティでも広く使われています。 ただし、ログデータの内容や扱い方は、企業の方針、ビジネスの性質、アプリケーションの種類に