>> 連載の目次は こちら!
今回は、処理ブロックやそのスコープを、Procでオブジェクト化する方法を学んでみたい。
かなりディープなお話だと思うので、入門編としては概要をつかむ程度になると思うが、ひと通りの扱い方や特徴については、ここで理解しておきたい。
■ 参考URL
■ Proc と lambda の概要
メソッドをオブジェクト化したのが、「Method」オブジェクト(これは 08. メソッドを定義する を参照)で、
ブロックをオブジェクト化したのが「Proc」オブジェクト。
ブロックがProcとしてオブジェクト化された時、そのブロック内の処理だけではなく、そのブロックが書かれた空間(スコープと同義?)も一緒に封じ込められる。
これは、いわゆる「クロージャ」をRubyで実現するものであり、Procの肝となる概念だが、複雑なので後述の例文で実際に動かして理解する。
lambda は、独自の記法でProcを生成する手段。
ただし、lambda記法で生成されたProcは、Procのnewやprocメソッドで生成されたProcオブジェクトとは微妙に振る舞いに違いがあるので注意が必要(後述)
■ 基本的な使い方
# ################################################
# Proc の生成
# ################################################
# Procのnewで生成
p1 = Proc.new { |str| puts str }
p p1 #<Proc:0x007fd1360cdfa0@/Users/miro/Documents/RubyProjects/MyRuby/mytest2.rb:2>
# proc メソッドで生成
p2 = proc { |str| puts str }
p p2 #<Proc:0x007fd1360cde88@/Users/miro/Documents/RubyProjects/MyRuby/mytest2.rb:5>
# lambda メソッドで生成
p3 = lambda { |str| puts str }
p p3 #<Proc:0x007fd1360cddc0@/Users/miro/Documents/RubyProjects/MyRuby/mytest2.rb:8 (lambda)>
# lambda メソッドの省略形
p4 = ->(str) { puts str }
p p4 #<Proc:0x007fd1360cdd20@/Users/miro/Documents/RubyProjects/MyRuby/mytest2.rb:9 (lambda)>
# lambda メソッドの省略形の引数なしパターン
p5 = -> { puts "I like it!" }
p p5 #<Proc:0x007fd1360cdc80@/Users/miro/Documents/RubyProjects/MyRuby/mytest2.rb:10 (lambda)>
# ################################################
# Proc の実行
# ################################################
# call で実行
# call の引数が、そのままProcの実行ブロックのパラメータとして渡される。↑上記の |str| の部分。
p1.call("wa-i")
p2.call("lalala-")
p3.call("hello")
p4.call("hey")
p5.call
# ※ちなみに、メソッドの回でも書いたが、上記の「call」メソッドは省略記法があるので注意。
p1["waiwai"]
p2.("gayagaya")
# ################################################
# メソッドに Proc を渡す
# ################################################
# メソッドの引数としてProcを渡して実行する
def exec_proc(arg, proc)
proc.call(arg)
end
p = ->(n) { puts n * n }
exec_proc(4, p) # 16
# メソッド側で、普通のブロックで受け取った引数をProcに変換して実行する
def exec_proc(arg, &callable)
callable.call(arg)
end
exec_proc(3) { |n| puts n * n } # 9
# 仮引数に&を付けると、引数を to_proc でProcに変換したうえで、callが実行される
# ちなみに、メソッドの回で解説したMethodオブジェクトも to_proc でProcに変換可能
# Procに「&」を付けて渡して、yieldで実行する
def exec_proc(arg)
yield(arg)
end
p = ->(n) { puts n * n }
exec_proc(5, &p) # 25
# 上記は、下記と同じこと
exec_proc(5) { |n| puts n * n } # 25
■ Proc の生成方法による微妙な違い
Proc は、Proc.new で生成した場合と、lambdaで生成した場合で、微妙に挙動が異なる。
その違いをここで整理しておく。
● return の挙動の違い
以下の通り、
Proc.new で生成したProc内でreturnすると、親空間(?)にあたるメソッドも抜けてしまう。
いっぽう、lambdaで生成したProc内でreturnしても、親空間(?)にあたるメソッドの処理は続行される。
def method1
p = Proc.new { return "Proc return!" }
p.call
return "method1 return!"
end
def method2
p = -> { return "lambda return!" }
p.call
return "method2 return!"
end
puts method1 # Proc return!
puts method2 # ethod2 return!
● break の挙動の違い
breakも、returnと同様、
Proc.new で生成したProcからbreakすると、親空間(?)にあたるループも抜けてしまう。
いっぽう、lambdaで生成したProcからbreakしても、親空間(?)にあたるループは続行される。
p = Proc.new { puts "Proc break!"; break; }
3.times do |i|
puts "loop1: #{i}"
p.call
end
# loop1: 0
# Proc break!
p = -> { puts "lambda break!"; break; }
3.times do |i|
puts "loop2: #{i}"
p.call
end
# loop2: 0
# lambda break!
# loop2: 1
# lambda break!
# loop2: 2
# lambda break!
● 引数が足りない時の挙動の違い
以下の通り、
Proc.new で生成したProcに渡された引数が足りなかった場合、渡されなかった引数はnilになる
いっぽう、lambdaで生成したProcに渡された引数が足りなかった場合、例外が発生する
proc = Proc.new { |arg1, arg2| p arg1, arg2 }
proc.call("wa-i")
# "wa-i"
# nil
proc = -> (arg1, arg2) { p arg1, arg2 }
proc.call("wa-i")
# Uncaught exception: wrong number of arguments (given 1, expected 2)
■ クロージャとしてのProcの挙動を確認する
前述の通り、ブロックが、Procとしてオブジェクト化された時、そのブロック内の処理だけではなく、そのブロックが書かれた空間(スコープと同義?)も一緒に封じ込められる。これが、他言語における、いわゆる「クロージャ」に該当する。
抽象的なので、実際のコードで試してみる。
# メソッドという `空間` を用意する。
# ある `空間` で生成された Proc には、
# 自分が生成された`空間`(この例ではメソッドのスコープ)までが、まるごと封じ込められている
def get_proc
# nという変数が存在する空間がある
n = 0;
# その空間でProcを生成して返す
->(inc) { n += inc; puts n }
end
# Procを生成してp1に代入
p1 = get_proc
3.times { p1.call(5) }
# 5
# 10
# 15
# p1というProcには、インクリメント処理だけでなく、その空間(この例ではget_procというメソッドのスコープ)が封じ込められている。
# そのため、上記のように同一のProcオブジェクトから複数回インクリメント処理を呼ぶと、
# 同じ空間に存在するnという変数は共有されて増えていくことが確認できる
# Procを生成してp2に代入
p2 = get_proc
3.times { p2.call(4) }
# 4
# 8
# 12
# こちらは新規にProcを生成しているので、先程の3回インクリメントとは別空間となっていることが分かる。
■ Proc をcase文の条件分岐に利用する
Procクラスでは、「===」がcallメソッドのエイリアスとして定義されている。
なので、何かしらの値とProcの戻り値を比較して、条件に該当するかどうかチェックすることができる。
これを利用して、case文のwhen節にProcを指定することができる。
def check_fun(chk)
is_event = -> (name) { ["GW", "夏休み", "クリスマス"].include?(name) }
case chk
when is_event then puts "楽しい! ヾ(@^▽^@)ノ"
else puts "つまんない (´・_・`)"
end
end
check_fun("夏休み")
# 楽しい! ヾ(@^▽^@)ノ