LoginSignup
5
4

【Ruby】ブロックとyield・Proc・lambda

Last updated at Posted at 2023-09-23

はじめに

yieldProclambda、こいつらを目にするたびに

  1. 「何だったっけなぁ、ブロックを扱う概念だった気がするけど、よくわからん」
  2. 調べて、軽くコードを書いて実行してみる
  3. わかった気がして満足する
  4. しばらく使うこともなく、1へ戻る

を繰り返していました。

そもそも「ほとんど使うことがない」というのも、理解が不十分だから使える場面を見逃しているのではないかという気もしてきたので、ここらで記事にまとめて、理解を深めていこうと思います。

実際にコードを書きながら挙動を確認していくので、似たようなことを感じている方はぜひ、手を動かしながらご覧ください!

「公式ドキュメントを読めばわかるだろ!」という話かもしれませんが、やっぱり自分でコードを書いて(さらに気になったことを思いつくまま試した方が)理解が深まると思います。

目次

検証環境

Ruby 3.0.5

ブロック

ブロックはメソッドを呼び出すときに記述できるコードのかたまりです。
{}またはdoendで囲むことで記述できます。
一行で書く場合は{}、複数行で書く場合はdoendが使われることが多いです。

試しに、メソッドなしでブロックを記述してみます。

sample.rb
do
  puts 'sample'
end
実行結果
sample.rb:1: syntax error, unexpected `do'
sample.rb:5: syntax error, unexpected `end', expecting end-of-input

エラーが発生し、メソッドなしでは記述できないことがわかります。

yield

yieldはブロックをメソッドの内部で呼び出すときに使います。

sample.rb
def sample(x, y)
  puts "yield:#{yield}"
  x + y + yield
end

puts "sample:#{sample(1, 2) { 3 }}"
実行結果
yield:3
sample:6

ブロックの戻り値である3がメソッド内のyieldに代入されていることがわかります。

さて、yieldを記述せずにメソッドにブロックを渡すとどうなるのでしょうか?
気になったことは試してみます。

sample.rb
def sample(x, y)
  x + y
end

puts "sample:#{sample(1, 2) { 3 }}"
実行結果
sample:3

メソッドに渡したブロックが無視されています。

エラーが発生しないので、使わないブロックを渡していても気が付きにくいので、気を付けていきたいところです。(そもそも、使わないブロックをうっかり渡す場面なんて、あんまりないか?)

では、逆にyieldを記述して、メソッドにブロックを渡さないとどうなるのでしょうか?
早速、試してみます。

sample.rb
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?というメソッドがあったので、こちらを使ってみます。

sample.rb
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に引数を渡すことで、ブロック内でその引数を使うことができるようなので、やってみます。

sample.rb
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

せっかく(?)なので、可変長引数に書き換えてみます。

sample.rb
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は、ブロックをオブジェクトとして扱うためのクラスです。

初めて聞いた時は、私はこんなのを想像していました。

sample.rb
# エラーになります
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メソッドを使用します。

引数も渡すことができます。

sample.rb
proc = Proc.new{ puts "abc" }
proc.call

proc = Proc.new{ |str| puts str }
proc.call("efg")
実行結果
abc
efg

メソッドにブロックとしてProcオブジェクトを渡したいときは、&を付けて、最後の引数にします。

sample.rb
def sample(x)
  x + yield
end

proc = Proc.new{ 3 }

puts sample(1, &proc)
実行結果
4

ここは、あまり試したいことが思いつかなかったので、次へ進みます。

lambda

lambdaは、Procオブジェクトを生成するためのメソッドです。

sample.rb
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によりインスタンスを生成すると引数の数が一致していない場合は、余分な引数を無視します。

sample.rb
proc = Proc.new{ |str| puts str }
proc.call("abc", "efg")
実行結果
abc

なお、引数の数が足りない場合は、nilを返します。

sample.rb
proc = Proc.new{ |str| puts str }
puts proc.call.nil?
実行結果
true

一方で、lambdaによりインスタンスを生成した場合は、引数の数が一致しないとエラーが発生します。

sample.rb
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>'

ジャンプ構文の挙動

returnbreak時の挙動が違うようなので、今回はreturnで試してみます。

Proc.newはブロック内でreturnすると、メソッドから脱出していることがわかります。

sample.rb
def proc_method
  proc = Proc.new{ |str| return str }
  proc.call("abc")
  return "end"
end

puts proc_method
実行結果
abc

一方で、lambdaはブロックを脱出するだけで、メソッドからは脱出していないことがわかります。

sample.rb
def lambda_method
  lambda = ->(str){ return str }
  lambda.call("abc")
  return "end"
end

puts lambda_method
実行結果
end

さいごに

yieldはあれこれ気になり色々と試しましたが、Proclambdaは調べたことをコードに落とし込むのが中心となりました。

それでも、サンプルのコードを単に写すのではなく、リファレンスや書籍で説明されている挙動を確認するためのコードを自分で書くことで、理解は深まったと感じています。

さて、ここまで書いて一つ問題が発生しました。
yieldProclambda、こいつらはいったいどこで使ったらいいんだ?」
やはり使い所のイメージが湧きません…

こちらは今後の課題として、業務のコードを追うなどしながら探していきます。

5
4
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4