Ruby の呼び出し可能オブジェクトについて
メタプロ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つの状態があります。
proc
lambda
今回はこの 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
オブジェクトから呼び出しを行う対象のオブジェクト
を取り除いた』になる