ブロックなし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かなりすごい(かなり)