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.newproc メソッドでブロックからオブジェクトを生成する事ができます。

# 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 を生成する

lambdalambda メソッドで定義する事ができます。 使い方は 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

ここで重要なのは proclambda も状態が違うだけで『両方共 Proc クラスのオブジェクトになる事』です。

proclambda の違い

proclambda は細かいところで違いがあるんですが、ここでは『引数の数が厳密かどうか』と『ブロック内で 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]

proclambda を判定するには 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 したときの挙動が proclambda で異なります。 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 オブジェクトには proclambda の2つの状態が存在している
    • proclambda で引数の数が厳密にチェックされるかどうかの違いなどがある
  • Method オブジェクトには UnboundMethod オブジェクトという似ているオブジェクトがある
    • UnboundMethod オブジェクトは『 Method オブジェクトから 呼び出しを行う対象のオブジェクトを取り除いた』になる