はじめに
yield
、Proc
、lambda
、こいつらを目にするたびに
- 「何だったっけなぁ、ブロックを扱う概念だった気がするけど、よくわからん」
- 調べて、軽くコードを書いて実行してみる
- わかった気がして満足する
- しばらく使うこともなく、1へ戻る
を繰り返していました。
そもそも「ほとんど使うことがない」というのも、理解が不十分だから使える場面を見逃しているのではないかという気もしてきたので、ここらで記事にまとめて、理解を深めていこうと思います。
実際にコードを書きながら挙動を確認していくので、似たようなことを感じている方はぜひ、手を動かしながらご覧ください!
「公式ドキュメントを読めばわかるだろ!」という話かもしれませんが、やっぱり自分でコードを書いて(さらに気になったことを思いつくまま試した方が)理解が深まると思います。
目次
検証環境
Ruby 3.0.5
ブロック
ブロックはメソッドを呼び出すときに記述できるコードのかたまりです。
{}
またはdo
とend
で囲むことで記述できます。
一行で書く場合は{}
、複数行で書く場合はdo
とend
が使われることが多いです。
試しに、メソッドなしでブロックを記述してみます。
do
puts 'sample'
end
sample.rb:1: syntax error, unexpected `do'
sample.rb:5: syntax error, unexpected `end', expecting end-of-input
エラーが発生し、メソッドなしでは記述できないことがわかります。
yield
yield
はブロックをメソッドの内部で呼び出すときに使います。
def sample(x, y)
puts "yield:#{yield}"
x + y + yield
end
puts "sample:#{sample(1, 2) { 3 }}"
yield:3
sample:6
ブロックの戻り値である3がメソッド内のyield
に代入されていることがわかります。
さて、yield
を記述せずにメソッドにブロックを渡すとどうなるのでしょうか?
気になったことは試してみます。
def sample(x, y)
x + y
end
puts "sample:#{sample(1, 2) { 3 }}"
sample:3
メソッドに渡したブロックが無視されています。
エラーが発生しないので、使わないブロックを渡していても気が付きにくいので、気を付けていきたいところです。(そもそも、使わないブロックをうっかり渡す場面なんて、あんまりないか?)
では、逆にyield
を記述して、メソッドにブロックを渡さないとどうなるのでしょうか?
早速、試してみます。
def sample(x, y)
puts "yield:#{yield}"
x + y + yield
end
puts "sample:#{sample(1, 2)}"
sample.rb:2:in `sample': no block given (yield) (LocalJumpError)
from sample.rb:6:in `<main>'
こちらはエラーとなりました。
メソッド内で使いたいブロックを受け取っていないので、当然といえば当然な気もします。
でもブロックのあり・なしで処理を分けたいこともありそうな気がします。
調べてみると、メソッドにブロックが与えられているかを返すblock_given?
というメソッドがあったので、こちらを使ってみます。
def sample(x, y)
puts "yield:#{yield}" if block_given?
x + y
end
puts "sample:#{sample(1, 2)}"
puts "="*10
puts "sample:#{sample(1, 2) { 3 }}"
sample:3
==========
yield:3
sample:3
無事、ブロックの有無で条件分岐をすることができました。
ブロックといえば、each
メソッドでレシーバーの要素を引数として受け取っているところをよく見ます。
# こんなイメージ
array = ['a', 'b', 'c']
array.each do |str|
puts str
end
というわけで、次はブロックに引数を渡してみたいと思います。
yield
に引数を渡すことで、ブロック内でその引数を使うことができるようなので、やってみます。
def sample(x, y)
x + y + yield(1, 2, 3)
end
z = sample(1, 2) do |args1, args2, args3|
puts "args: #{args1} #{args2} #{args3}"
args1 + args2 + args3
end
puts "sample:#{z}"
args: 1 2 3
sample:9
せっかく(?)なので、可変長引数に書き換えてみます。
def sample(x, y)
x + y + yield(1, 2, 3)
end
z = sample(1, 2) do |*args|
puts "args:#{args}"
args.sum
end
puts "sample:#{z}"
args:[1, 2, 3]
sample:9
うん、いい感じです(自己満足)
さて、最後は少し逸れてしまいましたが、yield
を好き放題いじったので、ここらで次に向かうこととします。
Proc
Proc
は、ブロックをオブジェクトとして扱うためのクラスです。
初めて聞いた時は、私はこんなのを想像していました。
# エラーになります
proc = { puts "abc" }
sample.rb:1: syntax error, unexpected string literal, expecting `do' or '{' or '('
proc = { puts "abc" }
sample.rb:1: syntax error, unexpected '}', expecting end-of-input
proc = { puts "abc" }
実際にProc
オブジェクトを作成するにはnew
メソッドを、呼び出す時にはcall
メソッドを使用します。
引数も渡すことができます。
proc = Proc.new{ puts "abc" }
proc.call
proc = Proc.new{ |str| puts str }
proc.call("efg")
abc
efg
メソッドにブロックとしてProc
オブジェクトを渡したいときは、&
を付けて、最後の引数にします。
def sample(x)
x + yield
end
proc = Proc.new{ 3 }
puts sample(1, &proc)
4
ここは、あまり試したいことが思いつかなかったので、次へ進みます。
lambda
lambda
は、Proc
オブジェクトを生成するためのメソッドです。
lambda = ->(){ puts "abc"}
lambda.call
lambda = ->(str){ puts str }
lambda.call("efg")
puts lambda.class
abc
efg
Proc
ぱっと見、Proc.new
と書き方が違うだけにしか見えません。
試しに、クラスを確認してみたところ、Procであることもわかりました。
どこが違うのかは次のセクションへ。
Proc.newとlambdaの違い
引数の扱い
Proc.new
によりインスタンスを生成すると引数の数が一致していない場合は、余分な引数を無視します。
proc = Proc.new{ |str| puts str }
proc.call("abc", "efg")
abc
なお、引数の数が足りない場合は、nil
を返します。
proc = Proc.new{ |str| puts str }
puts proc.call.nil?
true
一方で、lambda
によりインスタンスを生成した場合は、引数の数が一致しないとエラーが発生します。
lambda = ->(str){ puts str }
lambda.call("abc", "efg")
sample.rb:4:in `block in <main>': wrong number of arguments (given 2, expected 1) (ArgumentError)
from sample.rb:5:in `<main>'
ジャンプ構文の挙動
return
やbreak
時の挙動が違うようなので、今回はreturn
で試してみます。
Proc.new
はブロック内でreturn
すると、メソッドから脱出していることがわかります。
def proc_method
proc = Proc.new{ |str| return str }
proc.call("abc")
return "end"
end
puts proc_method
abc
一方で、lambda
はブロックを脱出するだけで、メソッドからは脱出していないことがわかります。
def lambda_method
lambda = ->(str){ return str }
lambda.call("abc")
return "end"
end
puts lambda_method
end
さいごに
yield
はあれこれ気になり色々と試しましたが、Proc
、lambda
は調べたことをコードに落とし込むのが中心となりました。
それでも、サンプルのコードを単に写すのではなく、リファレンスや書籍で説明されている挙動を確認するためのコードを自分で書くことで、理解は深まったと感じています。
さて、ここまで書いて一つ問題が発生しました。
「yield
、Proc
、lambda
、こいつらはいったいどこで使ったらいいんだ?」
やはり使い所のイメージが湧きません…
こちらは今後の課題として、業務のコードを追うなどしながら探していきます。