Ruby における関数オブジェクトとブロック引数とは…?

Ruby 2.6 で追加される Proc#>>Symbol も渡したいよねー内部で #to_proc も呼び出してほしいよねーと考えた時の覚書。

現在

  • Proc#>> には #call が定義されているオブジェクトを渡せる

なにをしたい

  • Proc#>>#to_proc が定義されているオブジェクトも渡したい
  • method(:hoge) >> foo.to_procmethod(:hoge) >> foo と書きたい
  • Proc#>>Symbol も渡したい
  • #to_proc を Refinements で定義されている場合にも渡したい
class Array
  # to_proc を Refinements で定義したい…
  def to_proc
    proc { |*args|
      self.map { |it| it.to_proc.call(*args) }
    }
  end
end

User = Struct.new(:id, :name, :age)

users = [
  User.new(1, "Homu", 14),
  User.new(2, "Mami", 15),
  User.new(3, "Mado", 14),
]

users.map &method(:pp) >> [:name, :age]

メモ

#call について

  • Ruby では #call が生えているオブジェクトは関数オブジェクトとして扱われる
    • ProcMethod など
  • meth(&block) のようにブロック引数で受け取って block.call のように呼び出す

#to_proc について

  • #to_proc は『関数オブジェクト』を返すメソッド
  • #to_proc はブロック引数のシンタックスシュガーとして使われる
    • &hoge の時に hoge.to_proc が呼ばれる
  • #to_proc が定義されているだけでは『関数オブジェクト』と呼べない
  • ブロック引数で渡したいと気に #to_proc を定義する

Proc#>> について

  • Proc#>> は関数オブジェクトを受け取る
    • 内部で受け取ったオブジェクトの #call を呼び出している
  • なので内部で #to_proc を呼び出すような挙動は一貫性がない
    • #to_proc が定義されているだけのオブジェクトは『関数オブジェクト』ではない
  • Proc#>>#to_proc が定義されているオブジェクトを受け取りたい場合は Proc#>>(a) ではなくて Proc#>>(&block) で受け取るべき
    • 結果的に #to_proc が定義されているオブジェクトを渡すことが出来る
  • しかし method(:hoge).<< do ... end みたいに『ブロック構文』を使うような使い方は想定していない(と思うので) Proc#>>(&block) を定義するのはおかしい
    • #to_proc を呼び出したいだけのために &block 引数を定義するのはおかしい
  • Proc#(a, &block) みたいに定義することで #call#to_proc の両方を受け取ることが出来る
    • ただし、 a&block の両方を渡した場合にどうするのか、という問題は残る

まとめ

任意の関数で、

  • 関数オブジェクトを受け取りたい
  • ブロック引数で受け取りたい
    • ブロック引数で受け取るという前提で meth(&block) 渡しが出来る

を切り分ける必要がある。 今回の Proc#>> は『ブロック引数』で受け取るのではなくて『関数オブジェクト』を受け取るので #to_proc を渡すのは難しそう Proc#>> のように『関数オブジェクト』を期待するメソッドに対しては #call を定義したオブジェクトを渡すべき

まとめ2

  • Proc#>>Symbol を渡したいのであれば
    • Symbol#call を定義すべき
  • Proc#>>#to_proc が定義されているオブジェクトを渡したいのであれば
    • Proc#>>(&block) のようにブロック引数で受け取るべき
  • #to_proc を呼び出すシンタックスシュガーがほしい…
    • 例えば ~hogehoge.to_procシンタックスシュガーであれば(~ は仮
    • method(:foo) >> :hoge.to_procmethod(:foo) >> ~:hoge とかけたり when :even?.to_procwhen ~:even? とかけたりする

そもそも…

次のような構文はシンタックスエラーになる…

class X
    def << &block
    end
end

# syntax error, unexpected &
X.new << &:hoge

備考

  • 関数オブジェクト:#call が定義されているオブジェクト
  • ブロッカブルオブジェクト:#to_proc が定義されているオブジェクト
  • 両対応する場合、どうするのがよいか

コード例

Proc#>> に渡したいのであれば以下のように #to_proc を定義するのではなくて

class Array
  def to_proc
    proc { |*args|
      self.map { |it| it.to_proc.call(*args) }
    }
  end
end

User = Struct.new(:id, :name, :age)

users = [
  User.new(1, "Homu", 14),
  User.new(2, "Mami", 15),
  User.new(3, "Mado", 14),
]

users.map &method(:pp) >> [:name, :age]

以下のように #call を定義するべき

require "pp"

class Array
  def call *args
    self.map { |it| it.to_proc.call(*args) }
  end
end

User = Struct.new(:id, :name, :age)

users = [
  User.new(1, "Homu", 14),
  User.new(2, "Mami", 15),
  User.new(3, "Mado", 14),
]

users.map &(method(:pp) << [:name, :age])