【Ruby】ブロックとProcとlambdaについて

スポンサーリンク

Ruby のブロックと Proc と lambda についてまとめました。

Procオブジェクトの生成

Proc オブジェクトを生成するにはProc.new Kernel.#proc Kernel.#lambdaにブロックを渡します。またはlambdaの別の記法でもあるアロー演算子が使えます。

pobj = Proc.new { |x| x + 1 }  # => #<Proc:0x007fb9ca80fda0@(irb):101>
pobj.call(1)  # => 2
pobj = proc { |x| x + 2 }  # => #<Proc:0x007fb9cb095678@(irb):103>
pobj.call(1)  # => 3
pobj = lambda { |x| x + 3 }  # => #<Proc:0x007fb9cb05e970@(irb):104 (lambda)>
pobj.call(1)  # => 4
pobj = ->(x) { x + 4 }  # => #<Proc:0x007f9aac8150f8@(irb):105 (lambda)>
pobj.call(1)  # => 5

class Proc (Ruby 2.4.0)
module function Kernel.#proc Kernel.#lambda (Ruby 2.4.0)

ブロックを受け取るメソッド

ブロックはメソッドに渡す無名引数のようなものです。メソッドの仲でyieldを使用すると受け取ったブロックが実行されます。yieldがあるのにブロックなしで呼び出すと例外で失敗します。

def my_method(arg)
  yield(arg)
end
my_method('Bob') {|name| "Hello, #{name}!"}  # => "Hello, Bob!"
my_method('Bob')
LocalJumpError: no block given (yield)
    from (irb):21:in `my_method'
  from (irb):23

ブロックを受け取ったかどうかはblock_given?で判定できます。

def my_method
  block_given?
end
my_method { 'hoge' }  # => true
my_method  # => false

ブロックを引数で明示的に受け取ることも可能です。その場合は引数の最後で名前の前に&修飾を付けます。&を付けると「メソッドに渡されたブロックを受け取ってProcに変換する」という意味になります。 引数で明示した場合でも省略した場合でもブロックは1つしか受け取れません

def my_method(arg, &my_block)
  yield(arg)
end
my_method('Ben') {|name| "Hello, #{name}!"}  # => "Hello, Ben!"

明示的にブロックを受け取る意味としては、ブロックをProcに変換したい時、または他のメソッドにブロックを渡したいときです。

# ブロックをProcに変換
def my_method(&my_block)
  my_block
end
obj = my_method {|x| x + 1}  # => #<Proc:0x007f9aac866430@(irb):37>
obj.call(1)  # => 2

# 他のメソッドにブロックを渡す
def do_method(arg)
  yield(arg)
end
def my_method(arg, &my_block)
  do_method(arg, &my_block)
end
my_method('Nick') {|name| "Hello, #{name}!"}  # => "Hello, Nick!"

といいつつブロックをProcに変換したい時でも明示的に引数で受け取らなくても変換できます。Proc.newまたはlambdaでブロックを指定しなければ、メソッドを呼び出しにブロックを伴うときに、それをProcオブジェクトとして生成して返すためです。

def my_method
  p = Proc.new
  p.call(2)
end
my_method {|v| v**v }  # => 4

ただしlambdaの場合は Warning がでます。意味的には「ブロック無しでProcオブジェクトを生成しようとしました」です。あまり分かりづらいコードになるのであまり使わない方がよさそうです。

def my_method
  p = lambda
  p.call(3)
end
my_method {|v| v**v }  # => 27
(irb):86: warning: tried to create Proc object without a block

Proc や lambda はオブジェクトなのでそのまま引数で受け取れます。

def my_method(pobj)
  pobj.call(2)
end

my_proc = proc { |v| v**v }
my_method(my_proc)  # => 4

メソッド呼び出し(super・ブロック付き・yield)
Rubyで使われる記号の意味(正規表現の複雑な記号は除く)

ブロックとProcの変換

そもそもブロック自体はオブジェクトではなく、ブロックがオブジェクトになったものがProcです。 &修飾を付けるとProcからブロックに変換されます。引数&を付けなければProcのままです。 &を付けなければというのは上述したブロックをProcに変換するで説明した引数で明示的に受け取った場合のことです。

def convert_proc(&my_block)  # &my_blockという変数が生成されるわけではなくmy_blockというProcで受け取る
  do_method(&my_block)  # 他のメソッドにブロックとして渡したい場合は&を付ける
  my_block  # &を付けなければProcのまま
end

Procを生成しておいて&を付けてブロックを受け取るメソッドに渡すことも可能です。

ary = [1,2,3,4,5]
my_proc = proc { |v| v**v }
ary.map &my_proc  # => [1, 4, 27, 256, 3125]

あらかじめProcを生成しなくても&を付けることでブロックに変換できます。

ary = [1,2,3,4,5]
ary.map &->(v) { v**v }  # => [1, 4, 27, 256, 3125]

Procとlambdaの違い

lambdaで生成した Proc は通常のものと違います。主な違いは引数のチェックreturnの挙動です。lambda の方がよりメソッドに近い働きをします。

引数チェック

引数のチェックに関しては lambda の場合、引数の数が違うと ArgumentError になりますが、Proc の場合は渡す引数が多いと、先頭から必要な数だけ取って後は無視し、少ないと足りない部分に nil を割り当てるため多重代入に近い扱い方をします。

l = lambda {|a, b| [a, b]}
l.call(1,2)  # => [1, 2]
l.call(1,2,3)
ArgumentError: wrong number of arguments (given 3, expected 2)
    from (irb):158:in `block in irb_binding'

p = Proc.new {|a, b| [a, b]}
p.call(1,2,3)  # => [1, 2]
p.call(1)  # => [1, nil]

returnとbreakとnextの挙動

lambda の場合は return を使うと lambda 内から外に戻ります。

def lambda_method
  l = lambda { return :foo }
  l.call
  return nil
end
p lambda_method
# => nil

Proc の場合は Proc 内から戻るのではなく、Proc が定義されたスコープから戻ります。

def proc_method
  pobj = proc { return :bar }
  pobj.call
  return nil
end
p proc_method
# => :bar

そのためトップレベルで定義した場合はスコープから戻れないため失敗します。

def proc_method(pobj)
  pobj.call
end
pobj = proc { return :bar }
proc_method(pobj)
LocalJumpError: unexpected return
    from (irb):269:in `block in irb_binding'
  from (irb):267:in `proc_method'

Proc でbreakをを使うと例外が発生します。 lambda はreturnと同じ挙動です。

def proc_method
  pobj = proc { break }
  pobj.call
  return nil
end
p proc_method
LocalJumpError: break from proc-closure
    from (irb):276:in `block in proc_method'
  from (irb):277:in `proc_method'

nextは lambda と Proc で同じで lambda 内から外に戻ります。

def proc_method
  pobj = proc { return :bar }
  pobj.call
  return nil
end
p proc_method
# => :bar

結論として lambda の場合はreturn break nextで同じ挙動をしますが、Proc でのreturn breakだけが定義されたスコープを抜けるという変わった挙動になるので注意が必要です。

ブロックのスコープ

ブロック (Procとlambdaも) を定義するとその時点でそのスコープにある束縛を取得します。そしてブロックをメソッドに渡した時はその束縛も一緒に渡ることになります。

def block_method
  x = 'inner'
  yield  # => "outer"
end
x = 'outer'
block_method {
  puts x
  x = 'foo'
}
puts x  # => 'foo'

逆にブロックの中で定義した変数などはブロックが終了した時点で消えます。

def block_method
  yield
end
block_method {
  y = 'bar'
}
puts y
NameError: undefined local variable or method `y' for main:Object