ブロックなしRubyをやることでRubyを関数型言語、というかLispっぽくやっていくことについて考えてみます。なお筆者はLispあんまり詳しく無く、「リストに対する操作の適用をネストさせてなんかいい感じにするやつ」くらいの認識で言いました、ごめん。
ブロックなしRuby初級
xs = %w`akechi kokoro itoh chika ayase ena koshimizu sachiko`
xs.map{|it|it.capitalize}
# => ["Akechi", "Kokoro", "Itoh", "Chika", "Ayase", "Ena", "Koshimizu", "Sachiko"]
このmapについて以下のように書くイディオムをご存知の方は多いかと思います。
xs.map &:capitalize
こうすることで冗長さがかなり軽減されます(かなり)。要素をイテレートし、各要素をレシーバとしてメソッドを呼び出したい、ただそれだけのためにitを2回書くなんて面倒ですからね。
では次のmapをブロックを書かずに書き直すとどうなるでしょうか。
xs = %w`明智小衣 伊藤千佳 綾瀬恵那 輿水幸子`
xs.map{|it|puts it}
ワウ
xs.map &method(:puts)
こうですね。eachメソッドについては忘れていきましょう。リストへの操作の適用について意識しだした結果mapしか使えなくなった人がいます。これがリストプロセッシング、Lispの気持ちなのでしょうか。違うと思う。違うのかな、わからん。
では次です。
xs = (1..10)
xs.map{|it|2 ** it}
# => [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
まずこれ、**演算子に見えるものは実のところメソッド呼び出しなので、以下の様に書き直せます。
xs = (1..10)
xs.map{|it|2.**(it)}
ぶわぶわ
xs.map &2.method(:**)
こうですね。
ここで、method(:puts)と2.method(:**)が何者なのか見てみましょう。
method(:puts).inspect
# => "#<Method: Object(Kernel)#puts>"
2.method(:**).inspect
# => "#<Method: Fixnum#**>"
感じ感じ
ブロックなしRuby中級
さて、いい感じにブロックを葬り去り簡潔でわかりやすいコードにすることができましたね。
ここで、先ほど挙げた3つの例をそれぞれ一般化してきましょう。
ただし、その前にmethodメソッドには退場してもらい、to_procを使う形で先ほどの例を書き直します。
# 1つ目
xs = %w`akechi kokoro itoh chika ayase ena koshimizu sachiko`
xs.map &:capitalize
1つ目はそのままです。
# 2つ目
xs = %w`明智小衣 伊藤千佳 綾瀬恵那 輿水幸子`
xs.map &:puts.to_proc.curry(2).call(Kernel)
2つ目、curryが出た。
# 3つ目
xs = (1..10)
xs.map &:**.to_proc.curry(2).call(2)
3つ目もcurryだ。
というわけで一般化です。
| ブロックあり | ブロックなし | ブロックなし(to_proc) |
|---|---|---|
| xs.map{|x| x.foo } | xs.map(&:foo) | xs.map(&:foo.to_proc) |
| xs.map{|x| foo(x) } | xs.map(&method(:foo)) | xs.map(&:foo.to_proc.curry(2).call(Kernel)) |
| xs.map{|x| obj.foo(x) } | xs.map(&obj.method(:foo)) | xs.map(&:foo.to_proc.curry(2).call(obj)) |
このようにして、Procオブジェクトを&prefix付きで渡すとブロックの様に振る舞うという仕様を利用することにより、好みのProcオブジェクトを作り出すことさえできればブロックを伴わずにmap操作が可能になるということがわかりました。(ちなみに&:fooの様にProcオブジェクトでなくシンボルを渡した場合は勝手にto_procされます。)
mapでなく普通のメソッド呼び出しとして一般化してみましょう。
| ブロックあり | ブロックなし(to_proc) |
|---|---|
| x.foo | :foo.to_proc.call(x) |
| foo(x) | :foo.to_proc.curry(2).call(Kernel).call(x) |
| obj.foo(x) | :foo.to_proc.curry(2).call(obj).call(x) |
Procオブジェクトを作ることで、任意のオブジェクトをレシーバとしてメソッド呼び出しを行うことができることがわかりました。
これをさらに一般化してみましょう。レシーバobjのメソッドfooを、任意のn個のオブジェクトを実引数として渡して実行するコードが以下になります。ただし、argsは任意のn個のオブジェクトを持つArrayインスタンスとします。
:foo.to_proc.curry(args.size + 1).call(obj).call(*args)
いいですね。Procオブジェクト最高。
ブロックなしRuby上級
ここで問題です。今までのコードを踏まえた上で、次のコードをProcオブジェクトを実行(callメソッド呼び出し)する形に書き換えてください。ただし、ブロックを使ってはいけません。
xs = (1..10)
xs.map{|it|it.to_s(2)}
# => ["1", "10", "11", "100", "101", "110", "111", "1000", "1001", "1010"]
…
……
………
どうですか?解けましたか?
実は、私の知る限り、ブロックを使わずにProcオブジェクトの部分適用にてこの問題を解く方法はありません。
というのも、RubyではProc#curryによってProcオブジェクトをカリー化することで、簡単に部分適用が行えるようになるものの、Symbol#to_procによって生成されたProcオブジェクトが第一引数としてメソッドを呼び出すレシーバのオブジェクトを取るという仕様をしているため、レシーバを未定にし、メソッドの実引数のみ部分適用したProcオブジェクトを生成することが(ブロックなしでは)不可能なのです。
先ほどの問題を無理やり解こうとすると、
xs.map(&:to_s.to_proc.curry(2)).
map(&:call.to_proc.curry(2)).
map(&:call.to_proc.curry(2)).
map(&:call.to_proc.curry(2)).
map(&:call.to_proc.curry(2)).
map(&:call.to_proc.curry(2)).
...
の様に無限にmapのネストが続いてしまいます。
1行目のxs.map(&:to_s.to_proc.curry(2))によって、mapの返り値として、第一引数、つまりメソッド呼び出し時のレシーバだけ部分適用したProcオブジェクトのEnumeratorが得られます。あとはそのProcオブジェクトに対して第二引数である2を渡す形でcallメソッドを実行してやればいいのですが、これは最初の目的であったEnumeratorの各要素をレシーバとして、引数を1つ取るメソッドを呼び出すという目的が、呼び出すメソッドがto_sからcallに変わった状態であることがわかるかと思います。つまり、カリー化したProcオブジェクトの部分適用という形でこの問題を解くことは不可能です。
というわけでこの問題の答えは、「each_with_objectを使う」です。以下にコードを示します。
xs.each_with_object(2).map &:to_s
each_with_objectによって生成されたEnumeratorは、yieldに2つの値を渡して実行します。これにより、mapを使いProcオブジェクトに2つの引数を一度に渡して実行するということが可能になります。
each_with_objectによって問題は解けたものの、Symbol#to_procによって得られたProcオブジェクトをカリー化、部分適用し、レシーバだけが未定な状態であるProcオブジェクトを作り出すことは不可能であるということがわかりました。(もちろん、ブロック、つまりlambdaを使えば可能です)
この問題は、引数の順序を入れ替えるProc#flipなどのメソッドがあれば解決します。この様に、あえてブロック禁止という縛りでプログラミングを行うことで、自然とProcやカリー化、部分適用などを用い、さらにEnumeratorへの操作適用をチェーンさせていくという、極めてLisp風とも言えるプログラミングの概念を自然に習得できますし、また同時に、せっかくProc#curryがあるのにProcに圧倒的にメソッド少なすぎるが故にProcオブジェクトをフル活用できないということを実感できるのではないかと思います。
まとめ
- あえてブロックなしRubyをやることで
Procの限界が見えてくる- ブロック本当に便利ということが再認識できる
- ブロックの利便性故にあんまり
Procに便利メソッド実装されないのかも
- lambda_driverかなりすごい(かなり)
- Nendoかなりすごい(かなり)