🦁

Rubyのblock/proc/lambdaについて調べてみた

2022/12/01に公開

挨拶

ポートでバックエンドを担当している新卒の @yuki.hara です。
最近 Ruby Silver の勉強のために、ブロック周りについて調べたのでそれについて書いていこうと思います。

そもそもブロックとは?

  • do end,{ }で囲むことでブロックは作成される
  • メソッドの引数に渡さなければいけなく、ブロック単体では存在できない。ブロック単体でも存在できる様にするために Proc クラス等を用いてオブジェクト化しないといけない

公式のリファレンスマニュアルでは、下記の様に定義されています。

ブロック付きメソッドとは制御構造の抽象化のために用いられるメソッドです。最初はループの抽象化のために用いられていたため、特にイテレータと呼ばれることもあります。 do ... end または { ... } で囲まれたコードの断片 (ブロックと呼ばれる)を後ろに付けてメソッドを呼び出すと、そのメソッドの内部からブロックを評価できます。ブロック付きメソッドを自分で定義するには yield 式を使います。

メソッド呼び出し(super・ブロック付き・yield) (Ruby 3.1 リファレンスマニュアル)

具体的なコード例で言うと下記になります。

def print_hello
  yield if block_given?
end

print_hello do
  p 'hello'
end

ブロックの特徴

Ruby のブロックはいわゆるクロージャにあたります。
クロージャは、生成時のコンテキスト(変数など)を保持している関数のことです。
生成時のコンテキスト(変数など)を保持しているため、本来メソッドの中から参照・更新等できない変数に対して参照・更新等を行うことができます。

# ブロックを用いない場合
values = []
def add_values
  values << 'test_value'
end
add_values() #=> NameError (undefined local variable or method `values' for main:Object)

# ブロックを用いた場合
values = []
def add_values
  yield if block_given?
end
add_values { values << 'test_value' }
p values #=> ["test_value"]

proc について

ブロックの基本は説明したので、次は proc についてみていきます。

proc は、ブロックをオブジェクト化したものです。ブロックの処理を呼ぶ出すときは、call メソッドで呼び出します。
使い方は下記のようになります。

hoge_proc = Proc.new { p 'hoge' }
hoge_proc.call

class Proc (Ruby 3.1 リファレンスマニュアル)

lambda について

lambda も proc と同様にブロックをオブジェクト化したものです。

hoge_proc = lambda { p 'hoge' }
hoge_proc.call

Kernel.#lambda (Ruby 3.1 リファレンスマニュアル)

proc と lambda の違い

proc と lambda はどちらもブロックをオブジェクト化したものなので、一見すると違いはない様に感じます。
しかし、違いはちゃんとあるので次から説明していきます。

主に違う点は、下記の2点になります。lambda で生成される手続きオブジェクトの方が、よりメソッドに近い挙動をする様に設計されているらしいです。

  • 引数の扱い
  • ジャンプ構文の挙動の違い

引数の扱い

メソッドと同様に lambda の方が引数の数を厳密にチェックします。

# proc
sample_proc = Proc.new do |a,b|
  p a, b
end

sample_proc.call(1)
# =>1
#   nil

# lambda
sample_proc = lambda do |a,b|
  p a, b
end

sample_proc.call(1)
# => ArgumentError (wrong number of arguments (given 1, expected 2)

ジャンプ構文の挙動の違い

lambda で作成されたブロックオブジェクト内で return を呼ぶと、手続きオブジェクトのスコープから抜け出します。
proc の場合は、手続きオブジェクトの呼び出し元のスコープから抜け出します。

  • return
# proc
def method_proc
  proc = Proc.new { return "proc"}
  proc.call
  "method_proc"
end

method_proc
#=> "proc"

# lambda
def method_lambda
  lambda1 = lambda { return "lambda"}
  lambda1.call
  "method_lambda"
end

method_lambda
#=> "method_lambda"

return, break, next を使用した際の詳しい挙動は下記記事が参考になりました!
return や break を使ったときの Proc.new とラムダの挙動の違い - Qiita

ブロックが役に立つ場面

メソッドを柔軟に拡張できたり、複数のメソッド内で一部のみ違いがある場合にその一部をブロックに切り出したりすることができる

同じ様な処理をしているが一部だけ違う処理をしている部分を、ブロックに切り出すことで下記の様なリファクタリングができたりする。

# リファクタ前
class Person
  def sunny_day_activity
    p 'stay home'
    p 'walk outside'
    p 'pray game'
  end

  def rainy_day_activity
    p 'stay home'
    p 'read book in my house'
    p 'pray game'
  end
end

Person.new.sunny_day_activity
Person.new.rainy_day_activity

# リファクタ後
class Person
  def activity
    p 'stay home'
    yield
    p 'pray game'
  end
end

Person.new.activity { p 'walk outside' }
Person.new.activity { p 'read book in my house' }

処理の呼び出しタイミングを遅延させることができる

下記の様に処理をオブジェクト化することで、呼び出すタイミングを自由に制御できる。

count = 0
count_proc = Proc.new { count += 1 }

count_proc.call
count #=> 1

count_proc.call
count #=> 2

まとめ

今回、苦手だったブロック周りについて調べてある程度深ぼることができたのでよかったです。
ちなみに Ruby Silver は無事合格できました!

Discussion