メタプロRuby本を参考にしつつ Ruby の呼び出し可能オブジェクトについてまとめてみました。
呼び出し可能オブジェクトとは
Ruby のブロックは『呼び出すメソッドに対して何かしらの処理を外から渡す』記法になります。
# 『配列の要素を2倍にする』という処理をブロックで記述する pp [1, 2, 3, 4, 5].map { |it| it * 2 } # => [2, 4, 6, 8, 10] # 『要素から偶数の値だけを抽出する』という処理をブロックで記述する pp [1, 2, 3, 4, 5].select { |it| it.even? } # => [2, 4]
呼び出し可能オブジェクトとはこの『ブロックの処理(など)』を『Ruby のオブジェクト』として扱うための概念になります。 この『呼び出し可能オブジェクト』は Ruby で言うと以下の2つのクラスが存在しています。
更に Proc クラスには2つの状態があります。
proclambda
今回はこの Proc オブジェクトや Method オブジェクトに関して解説していきます。
Proc オブジェクト
Ruby のブロックをオブジェクトとして扱う場合は Proc オブジェクトを使用します。
これは Proc.new や proc メソッドでブロックからオブジェクトを生成する事ができます。
# Proc.new に渡したブロックをオブジェクト化する plus = Proc.new { |a, b| a + b } pp plus.class # => Proc # proc は Proc.new しているのと同じ意味 plus2 = proc { |a, b| a + b } pp plus2.class # => Proc
この時点ではまだブロックの処理は呼び出されません。
ブロックの処理を呼び出すには Proc#call メソッドを呼び出します。
plus = proc { |a, b| a + b } # proc に渡したブロックを呼び出す # Proc#call に渡した引数が proc に渡したブロックに渡される pp plus.call(1, 2) # => 3 # 何度もブロックの処理を呼び出すことができる pp plus.call(3, 4) # => 7 pp plus.call(5, 6) # => 11
この『ブロックの処理を呼び出す事』を『評価する』といいます。 またこのように『ブロックをオブジェクト化してあとから処理を呼び出すこと』を『あとで評価する』や『遅延評価』などと呼ばれます。
ブロック引数に Proc オブジェクトを渡す
Proc オブジェクトは & を付ける事で他のメソッドのブロック引数として渡すこともできます。
def hoge # ブロックを評価する pp yield(1, 2) # => 3 end plus = proc { |a, b| a + b } # & を付けて渡すことで Proc オブジェクトをブロック引数として渡すことができる hoge(&plus)
ブロック引数は Proc オブジェクトとして受け取る
以下のようにブロックを評価する場合は yield を用いて評価する事ができます。
def hoge pp yield(1, 2) # => 42 end hoge { |a, b| a + b }
これとは別に & を付けて引数を定義する事で明示的に『ブロックをオブジェクトとして受け取る』事もできます。
# block という引数でブロックのオブジェクトを受け取る def hoge(&block) end
この block という変数が Proc オブジェクトとして値を受け取ります。
# block は Proc オブジェクトとして受け取る def hoge(&block) pp block.class # => Proc pp block.call(1, 2) # => 3 end hoge { |a, b| a + b }
このように明示的にブロック引数を記述することで『他のメソッドにブロック引数を渡すこと』ができます。
def foo pp yield(1, 2) # => 3 end def hoge(&block) # 他のメソッドにブロック引数を渡す foo(&block) end hoge { |a, b| a + b }
ブロック引数以外に Proc オブジェクトをメソッドに渡す例
ブロック引数でなくても Proc オブジェクトをメソッドに渡すことはできます。
def hoge(obj) obj.call(1, 2) end plus = proc { |a, b| a + b } pp hoge(plus) # => 3
これは例えば Enumerable#find のように『複数の呼び出し可能オブジェクトをメソッドに渡したい』場合に利用します。
# 最初の3よりも大きい数を探す # 見つからない場合は nil を返す p [1, 2, 3, 4, 5].find { |it| it > 3 } # => 4 p [2, 2, 2, 2, 2].find { |it| it > 3 } # => nil # find の第一引数に呼び出し可能オブジェクトを渡すことで『見つからなかったときの処理』を制御できる # 見つからなかった場合に第一引数の Proc オブジェクトが評価される p [2, 2, 2, 2, 2].find(proc { "見つかりませんでした" }) { |it| it > 3 } # => "見つかりませんでした" # error: `block in <main>': ないYO!! (RuntimeError) p [2, 2, 2, 2, 2].find(proc { raise "ないYO!!" }) { |it| it > 3 }
lambda を生成する
lambda は lambda メソッドで定義する事ができます。
使い方は proc メソッドと同じように lambda メソッドにブロックを渡してオブジェクト化します。
plus = lambda { |a, b| a + b }
lambda メソッドで生成したオブジェクトもまた Proc オブジェクトとなります。
plus = lambda { |a, b| a + b } # lambda メソッドで生成したオブジェクトも Proc オブジェクトになる pp plus.class # => Proc # proc と同じように Proc#call でブロックを評価する事ができる pp plus.call(1, 2) # => 3
また lambda は -> {} という特別な記法で定義する事もできる。
ブロックの引数を定義する位置が {} の内側でないので注意する。
# lambda { |a, b| a + b } と同じ意味 plus = -> (a, b) { a + b } pp plus.call(1, 2) # => 3
ここで重要なのは proc も lambda も状態が違うだけで『両方共 Proc クラスのオブジェクトになる事』です。
proc と lambda の違い
proc と lambda は細かいところで違いがあるんですが、ここでは『引数の数が厳密かどうか』と『ブロック内で return したときの違い』に絞って説明します。
引数の数が厳密かどうか
proc の場合はブロックで定義された引数の数と実際に渡された引数の数が違っていてもエラーにはなりません。
block = proc { |a, b| [a, b] } # 定義された引数分のを渡す pp block.call(1, 2) # => [1, 2] # 定義された引数よりも多いを渡してもエラーにならない # その場合はが切り捨てられる pp block.call(3, 4, 5, 6) # => [3, 4] # 定義された引数よりも少ないを渡してもエラーにならない # その場合はが nil になる pp block.call(7) # => [7, nil]
一方で lambda の場合はブロックで定義された引数の数と実際に渡された引数の数が違うとエラーになります。
block = lambda { |a, b| [a, b] } # 定義された引数分のを渡す pp block.call(1, 2) # => [1, 2] # 定義された引数よりも多いを渡すとエラーになる # error: `block in <main>': wrong number of arguments (given 4, expected 2) (ArgumentError) pp block.call(3, 4, 5, 6) # 定義された引数よりも少ないを渡すとエラーになる # error:`block in <main>': wrong number of arguments (given 1, expected 2) (ArgumentError) pp block.call(7) # => [7, nil]
proc と lambda を判定するには Proc#lambda? が利用できます。
proc_obj = proc {} lambda_obj = lambda {} pp proc_obj.lambda? # => false pp lambda_obj.lambda? # => true
またメソッドで受け取ったブロック引数は proc として受け取ります。
def hoge(&block) pp block.lambda? # => false end hoge {}
ブロック内で return したときの違い
ブロック内で return したときの挙動が proc と lambda で異なります。
proc 内で return すると『そのブロックを評価したメソッドから』抜けます。
def hoge block = proc { # ここで return すると hoge メソッドから return する return 42 } # call を呼び出すと hoge メソッドから return してしまう block.call # なので以下の処理は呼び出されない pp "call 後" end pp hoge # => 42
lambda 内で return すると『そのブロック内から』抜けます。
def hoge block = lambda { # ここで return するとブロック内から return する return 42 } # call を呼び出すとブロック内の return が返ってくる pp block.call # => 42 # 以下の処理も呼び出される pp "call 後" end hoge
以下のようにメソッドのブロック内で return する場合は気をつける必要があります。
def check [1, 2, 3, 4].each { |it| if it.even? # ここで return すると check メソッドから抜けてしまう return end } end
Ruby では意識して lambda を使わない限りは proc としてブロックを扱うことが多いので proc の挙動に対して注意しておく必要があります。
Method オブジェクト
Method オブジェクトは『メソッドを呼び出し可能オブジェクトとして扱うためのオブジェクト』になります。
Method オブジェクトは #method という特別なメソッドを用いてオブジェクトを生成します。
また Proc オブジェクトと同様に Method#call で評価する事ができます。
class Value def initialize(value) @value = value end def plus(a) @value + a end end value = Value.new(3) # 通常のメソッド呼び出し pp value.plus(4) # => 7 # x の plus メソッドを呼び出し可能オブジェクトとして生成する plus = value.method(:plus) # Proc ではなくて Method クラスのオブジェクトになる pp plus.class # => Method # Method#call でメソッドを評価する pp plus.call(6) # => 7
#method メソッドは全てのオブジェクトで定義されているので次のように呼び出す事もできます。
# String#upcase をオブジェクト化する upcase = "string".method(:upcase) # upcase メソッドを評価する pp upcase.call # => "STRING" # Integer#+ メソッドを Method オブジェクト化する plus = 1.method(:+)
Method オブジェクトをブロック引数に渡す
Method オブジェクトも Proc オブジェクトと同様に & を付けることでメソッドのブロック引数に渡すことができます。
class Value def initialize(value) @value = value end def plus(a) @value + a end end value = Value.new(3) plus3 = value.method(:plus) # map メソッドの内部で Value#plus メソッドが呼び出される pp [1, 2, 3].map(&plus3) # => [4, 5, 6]
UnboundMethod オブジェクト
Method オブジェクトは
- 呼び出し可能オブジェクト
- 呼び出しを行う対象のオブジェクト
の2つの情報を保持しています。
呼び出しを行う対象のオブジェクト は Method#receiver で取得する事ができます。
upcase = "string".method(:upcase) # upcase という呼び出しの対象が "string" オブジェクトになる pp upcase.receiver # => "string"
この『 呼び出しを行う対象のオブジェクト 』を Method オブジェクトから取り除いたものが UnboundMethod オブジェクトになります。
UnboundMethod オブジェクトの生成方法は2つあります。
Module.instance_method から生成する
Method.instance_method から UnboundMethod を生成することができます。
# String の upcase インスタンスメソッドの `UnboundMethod` オブジェクトを生成する upcase = String.instance_method(:upcase) pp upcase.class # => UnboundMethod
UnboundMethod に対して UnboundMethod#bind を使用することで対象の 呼び出しを行う対象のオブジェクト を割り当てる事ができます。
# Module.instance_method で任意のメソッドの `UnboundMethod` を生成できる upcase = String.instance_method(:upcase) # bind することで Method オブジェクト化することができる upcase_string = upcase.bind("string") pp upcase_string.class # => Method pp upcase_string.receiver # => "string" # bind したオブジェクトに対して upcase メソッドを呼び出す pp upcase_string.call # => "STRING" # 他のオブジェクトも bind することができる upcase_ruby = upcase.bind("ruby") pp upcase_ruby.class # => Method pp upcase_ruby.receiver # => "ruby" pp upcase_ruby.call # => "RUBY"
また .instance_method を呼び出したクラス以外のオブジェクトを bind するとエラーになります。
upcase = String.instance_method(:upcase) # error: `bind': bind argument must be an instance of String (TypeError) upcase.bind(42)
Method オブジェクトから UnboundMethod オブジェクトを生成する
Method#unbind を使用すると Method オブジェクトから 呼び出しを行う対象のオブジェクト が削除された UnboundMethod オブジェクトを生成します。
upcase = "string".method(:upcase) # unbind で UnboundMethod オブジェクトが生成される unbind = upcase.unbind pp unbind.class # => UnboundMethod # UnboundMethod#bind で別のオブジェクトを束縛できる pp unbind.bind("ruby").call # => "RUBY"
まとめ
- 呼び出し可能オブジェクトとは『処理をオブジェクト化したもの』になる
- あとから任意のタイミングで『処理』を呼び出すことができる
- 呼び出し可能オブジェクトの『処理』を呼び出すことを『評価する』と呼ぶ
- Ruby の呼び出し可能オブジェクトは大きく分けると2種類ある
Procオブジェクト- ブロックをオブジェクト化した
Methodオブジェクト- メソッドをオブジェクト化した
Procオブジェクトにはprocとlambdaの2つの状態が存在しているprocとlambdaで引数の数が厳密にチェックされるかどうかの違いなどがある
MethodオブジェクトにはUnboundMethodオブジェクトという似ているオブジェクトがあるUnboundMethodオブジェクトは『Methodオブジェクトから呼び出しを行う対象のオブジェクトを取り除いた』になる