Ruby の Proc オブジェクトと Method オブジェクトの違い (proc, lambda, ブロック, メソッドについて)

プログラミング言語 Ruby

プログラミング言語 Ruby

Ruby には、手続きを表すオブジェクトのクラスとして Proc クラスと Method クラスの 2 つのクラスがあります。 Proc オブジェクト *1 にはさらに proc と lambda の 2 種類があって、それぞれどう違うかというのがちょっとややこしいと思います。 次の記事などは結構参考になると思いますが、『プログラミング言語 Ruby』 を読んでさらに理解が深まったのでここにまとめておきます。

とりあえずまとめ

Proc オブジェクトや Method オブジェクト、proc、lambda、ブロック、メソッドについてまとめると次のようになります。 詳しい説明は記事中で行います。

  • Proc オブジェクトはクロージャ
  • Method オブジェクトは クロージャではなく *2、レシーバと結び付けられているのが特徴
  • Proc オブジェクトは proc と lambda の 2 種類に分けられる (大文字と小文字の違いに注意)
  • proc はブロックをオブジェクト化したもの
  • lambda は Method オブジェクトに近い動きをする (メソッドとの違いはクロージャかどうか)
  • Method オブジェクトはメソッドをオブジェクト化したもの

クロージャ (closure) とは

まずクロージャとは何かを説明します。 呼び出せる関数であると同時に、その関数の変数束縛でもあるオブジェクトのことをクロージャといいます。 ECMAScript (JavaScript) などでよく使われているように思います *3

変数束縛というのは、引数以外の変数を実行時の環境ではなく自身が定義された環境 (静的スコープ) で解決する、ということを意味しています。 例えば Ruby のブロックは、ブロックの外側の変数にアクセスすることができますが、これはブロックがクロージャだからです。

次のコードを実行すると、「ブロックを生成するメソッド」 と表示されます。 ブロックはクロージャなので、自身が定義された test メソッドのスコープで変数名を解決し、その結果 local_var の値として "ブロックを生成するメソッド" という文字列を出力するわけです。

def eval_closure( &block )
local_var = "ブロックを実行するメソッド"
# ブロックを実行するのはここ
block.call()
end

def test()
# ブロック内の変数はブロック生成時のスコープで探索されるので
# ブロック内の変数 local_var はこの変数を参照する
local_var = "ブロックを生成するメソッド"
# ここでブロックを生成する
eval_closure() { puts local_var }
end

test()

proc の作り方

Proc オブジェクトには proc と lambda の 2 種類があります。 ブロックをオブジェクト化したものが proc で、より関数に近い動きをするのが lambda です。 proc は、ブロックをオブジェクト化したものなので、次のように取得することができます。

# 引数として渡されたブロック (をオブジェクト化したもの) をそのまま返すメソッド
def return_block( &block )
return block
end
# ブロックをオブジェクト化したものを取得
proc_obj = return_block { puts "proc です" }
# クラスは Proc クラス
puts proc_obj.class #=> Proc
# proc か lambda かを見分ける (Ruby 1.9 のみ)
puts ( proc_obj.lambda? ? "lambda" : "proc" ) #=> proc

Proc オブジェクトは proc と lambda の 2 通りにわけられると説明しましたが、Ruby 1.9 では上の例の最後のように Proc#lambda? メソッド を使って proc か lambda かを判別することができます。

もちろんこのような回りくどいことをしなくても、Proc::new メソッド を使用することで proc を取得できます *4。 Proc::new メソッドにブロックを渡すことで、そのブロックがオブジェクト化されて proc として返されます。 また、Proc オブジェクトは Proc#call メソッド を呼び出すことで実行できます。

proc_obj = Proc.new { |arg1, arg2|
puts arg1
puts arg2
}
# Proc オブジェクトの手続きの実行
proc_obj.call( "test", "proc?" ) # 引数として渡した文字が表示される

lambda の作り方

lambda は、Kernel#lambda メソッド を使用して取得することができます。

lmd_obj = lambda { |arg1| puts arg1 }
puts lmd_obj.class #=> Proc
puts ( lmd_obj.lambda? ? "lambda" : "proc" ) #=> lambda

Ruby 1.9 では lambda リテラルも導入されました。 次のように、-> 記号の後ろに括弧でくくって *5 引数リストをとり、最後に {...} または do ... end で処理内容を書くことで lambda を得られます。

# lambda リテラルにより lambda を取得
lmd_obj = ->( arg1 = "test", arg2 = "lambda" ) do
puts arg1
puts arg2
end
# lambda の実行
lmd_obj.call()

lambda リテラルの優れているところは、メソッドと同様に引数のデフォルト値が定義できることです。 また、ブロックを受け取ることもできます。

proc と lambda の違い

proc と lambda は両方とも Proc オブジェクトですが、2 つの違いがあります。 1 つは制御フローが異なるということで、もう 1 つは引数の扱いが異なる、ということです。

まず制御フローに関してですが、proc はブロックと同じような制御フローをたどります。 たとえば、proc 内で return が実行された場合、proc の処理が終わるのではなく、proc を呼び出したメソッドが終了します。

def proc_test()
proc_obj = Proc.new { puts "メソッド終了!"; return }
proc_obj.call()
puts "proc_obj を実行するとこのメソッドが終了するので、この文字列は出力されない"
end
proc_test()

lambda はメソッドに近く、lambda 内で return を実行した場合は、lambda の処理が終わるだけです。

def lambda_test()
lmd_obj = ->(){ puts "lambda 終了!"; return }
lmd_obj.call()
puts "lmd_obj を実行してもメソッドは終了しないので、この文字列は出力される"
end
lambda_test()

break も proc と lambda でフローが変わります。 普通の proc の場合、break を使用した時点で LocalJumpError が発生します ((ブロックとしてメソッドに渡されて & 付き引数で受け取った proc は期待通りに動きます。))。 これは、proc が実行された時点で戻り先の実行が終わっているためです。 一方で、lambda 内で break を実行した場合は、単純に return と同じように動きます。 その他の制御フローに関しては、proc も lambda も同じように動きます。

また、引数の扱いですが、 proc は yield によるブロック呼び出しのときの引数の扱いと同じで、lambda はメソッド呼び出し時の引数の扱いと同じようになります。

proc の場合は引数の個数が異なっていてもエラーにならず、柔軟に対応されます。 仮引数の数が実引数よりも多い場合は、足りない部分に nil が入ります。 実引数のほうが多い場合は、多い分だけ切り捨てられます。 また、複数の仮引数に対して 1 個の配列が渡された場合は配列が展開されますし、1 個の仮引数に対して複数の引数を渡した場合は配列にまとめられます。

lambda の場合は、メソッド呼び出しのときと同じく個数が異なる場合はエラーになります *6

Method オブジェクトの作り方

ここまで Proc オブジェクト (proc および lambda) について説明しましたが、それに似たものとして Method オブジェクトがあります。 Method オブジェクトはメソッドをオブジェクト化したものです。 取得したいメソッド名のシンボルを引数にして Object#method メソッド を呼び出すことで Method オブジェクトを取得できます。

# もともとのオブジェクト
str = "Abc"
# Str#reverse! メソッドを Method オブジェクトとして取得
mtd_obj = str.method( :reverse! )
# mtd_obj のクラスは Method
puts mtd_obj.class #=> Method
# mtd_obj を実行すると, str.reverse!() と同じ結果が得られる
puts mtd_obj.call() #=> cbA
# mtd_obj は str のメソッドをオブジェクト化したものなので副作用はもとのオブジェクトに現れる
puts str #=> cbA

Proc オブジェクトと Method オブジェクトの違い

最後に Proc オブジェクト (proc および lambda) と Method オブジェクトの違いを説明します。

Proc オブジェクトと Method オブジェクトの最も大きな違いはクロージャかどうかである、と書籍には書いています。 メソッド定義の際に def method_name; ... end でくくったコードの中は、局所変数のスコープが外部と独立しているためにメソッドはクロージャにはなりえない、というのがその理由です。

基本的にはその通りなのですが、例外として Proc オブジェクトを元にして定義されたメソッドはクロージャになりえます。 Proc オブジェクトを元にメソッドを定義するには、Module#define_method メソッド を使う方法や、Object#define_singleton_method (Ruby 1.9 のみ) を使う方法があります。 以下の例のように lambda を元にしてメソッドを定義すると、メソッドの外にある局所変数 local_var を参照するメソッドが定義されます。 もちろんこれはクロージャです。

class A
local_var = 0
define_method( :test_a, ->() { puts local_var += 1 } )
end
a1 = A.new
a2 = A.new

# クロージャなので実行のたびに結果が変わる
# (もちろんクロージャじゃなくてもインスタンス変数を使えば同じことは可能)
a1.test_a #=> 1
a1.test_a #=> 2
a2.test_a #=> 3

しかし、これは Proc オブジェクトを元に作られたメソッドなので、例外的な扱いでいいと思います。 基本的には Method オブジェクトはクロージャではありません。 ちなみに、define_method メソッドに渡された Proc オブジェクトは instance_eval によって実行されるため、selfインスタンス変数 (@ 付き変数) は、もともとの Proc オブジェクトのスコープではなくメソッドのレシーバのスコープで参照されます。 局所変数はもとのスコープで参照されるのに対して、インスタンスに関連する変数はレシーバのスコープで参照されるという違いが少しややこしいので注意する必要があります。

また、Method オブジェクトは名前の通りメソッドを表現するオブジェクトなので、レシーバと結びついているという特徴があります。 Method#receiver メソッド でレシーバオブジェクトを取得できますし、Method#owner メソッド でメソッドがどのクラス (またはモジュール) で定義されているのか調べることができます。 一方の Proc オブジェクトは単なる手続きを表現するオブジェクトであり、レシーバは存在しません。

ただ、実際に使う場合の観点で考えると、Method オブジェクトは Method#to_proc メソッド で Proc オブジェクトに変換できますし、Proc オブジェクトも Method オブジェクトも call メソッドで実行できますし、あまり違いを認識する必要はないような気がします。 レシーバのない手続きは Proc オブジェクト (特に lambda) を使い、既に定義されているメソッドをオブジェクト化する際には Method オブジェクト (またはそれを Proc 化したもの) を使う、という風に使い分ければいいのではないでしょうか。

*1:Proc クラスのインスタンスのこと。 本記事で XXX オブジェクトというときは、全て XXX クラスのインスタンスを表す。

*2:Proc オブジェクトを元に作られたメソッドは例外的にクロージャになる。 詳しくは最後に述べる。

*3:と個人的に思ってますが、実際によく使われているかどうかのデータは持ってないので実際にはあまり使われていないかもしれません。

*4:Ruby 1.9 では Proc::new メソッドの代わりに Kernel#proc メソッド を使うことができる。 しかし、Ruby 1.8 では Kernel#proc メソッドは Kernel#lambda メソッドの別名であり、Ruby のバージョンによって動作が変わるので proc メソッドは使用しない方がよい。

*5:メソッド宣言と同様に、括弧は省略可能。

*6:デフォルト値が設定されている場合はこの限りではない。