methodオブジェクトによる関数型ライクなRuby記法

Railsアプリケーションの改修のため修正箇所のコードを見ていたらこんなコードがあった。

array.each_with_object(a: {}, b: {}, &method(:increment))

def increment(n, aggr)
  aggr[:a] = n + 1
  aggr[:b] = n + 2
end

※変数名、処理内容はダミー

Ruby特有の書き方に慣れていなかったため面食らってしまったが調べていくとRubyをふんだんに活用した記法だと理解できた。

each_with_objectのようなコレクションに対して何か処理を加えるようなメソッドを使うときはブロックを渡すのが一般的だと思う。

[1, 2, 3].map { |n| n+1 }

ちなみにselfをレシーバにしたメソッド呼び出し、かつ引数無しであればSymbolをProcに変換する省略記法が使える

[1, 2, 3].map(&:to_i)

しかし当該のコードでは&method(:increment)を渡している。

まず&無しのmethod(:increment)の部分で得ているものに関して説明するとこれはmethodオブジェクトというものでProcなどと同じくcallで評価ができるオブジェクト。

def hello
  'world'
end

method(:hello).call
=> "world"

このようにメソッドを持ち運びできるようにして関数型言語のような操作ができる。
(一般的に関数型言語では関数は第一級オブジェクトとなっている。)

そして&method(:increment)の&の部分だがこれは引数をブロックにして渡すという記法だ。 つまりyieldによって実行ができる。

def hello(n)
  yield n
end

hello(1, &:to_s)
=> "1"

ブロックとして渡すので仮引数の最後に渡さないといけない、また実引数にはカウントされない点に注意。

ここまでの話をまとめると

objects.each_with_object(a: {}, b: {}, &method(:increment))

の最後の引数に関してはincrementメソッドをmethodオブジェクト化してからさらにブロックとして展開して渡すという意味になる。
(ちなみに関数型言語であれば素直に関数名を渡して、受け取り側でも関数をそのまま評価するだけで良い。)

もう一つ疑問が残る、each_with_objectには一つの引数しか渡せない気がする。
この部分もRubyの記法だ。
Rubyでは最後の引数に限りハッシュの波括弧を省略して渡すことができる。

例えば

def hello(n, m)
  m
end

hello 1, a: 2, b: 3
=> {:a=>2, :b=>3}

と言った具合だ。 これで何が書いてあるか理解ができた。

objects.each_with_object(a: {}, b: {}, &method(:increment))

はeach_with_obejctの第一引数に{a: {}, b: {}}というハッシュを渡して、ブロックとしてincrementメソッドオブジェクトを展開する、ということ。

この1行にRubyのエッセンスが詰まっていてとても勉強になった。
methodオブジェクトを渡すようなことはそう多くないかもしれないが、処理の複雑性が高くなってしまった場合に可読性が上がる手段として良いと感じた。
テスタビリティも上がると思うので場面によって使い分けていきたいと思った。