5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[Ruby入門] 16. Proc と lambda(処理ブロックと空間をオブジェクト化する)

Last updated at Posted at 2017-05-04

>> 連載の目次は こちら!

今回は、処理ブロックやそのスコープを、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("夏休み")
# 楽しい! ヾ(@^▽^@)ノ
5
4
2

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?