RubyのSymbol#to_procを考えた人になってみる
ブログを下記に移転しました。デザイン変更により移転先では記事が一層読みやすくなっていますので、よろしければ移動をお願い致します。
RubyのSymbol#to_procを考えた人になってみる : melborne.github.com
Rubyのメソッドはブロックを取れる
ブロックはコードの塊だから
その内容に応じてメソッドの挙動を
大きく変化させることができるんだ
例えばinjectメソッドはリストタイプのオブジェクトに対して
たたみこみ演算を実行するものだけれど
これに加算を行うコードブロックを渡せば
injectメソッドはたたみこみ加算器となり
(1..10).inject(5) { |mem, var| mem + var } # => 60
一方乗算を行うコードブロックを渡せば
たたみこみ乗算器となるんだ
(1..5).inject(2) { |mem, var| mem * var } # => 240
またmapメソッドはリストの各要素に
同じ評価を与えるものだけれど
これにcapitalizeメソッドのコードブロックを渡せば
mapメソッドはcapitalize変換器となり
["ruby", "c", "lisp", "smalltalk"].map { |item| item.capitalize } # => ["Ruby", "C", "Lisp", "Smalltalk"]
一方lengthメソッドのコードブロックを渡せば
長さ演算器となるんだ
["ruby", "c", "lisp", "smalltalk"].map { |item| item.length } # => [4, 1, 4, 9]
もちろんブロックにはもっと複雑なコードを渡せる
でも意外と上で示したような単純な演算をさせることが多いよね
そうするとただ各要素の加算をしたり長さを求めるときに
いちいちmemとかitemとかのブロック変数を書くのが面倒くさい
なんとかならないかな…
(1..10).inject(5, :+) # => 60
とか書けたらすてきだなあ…
基の式はこうだから
(1..10).inject(5) { |mem, var| mem + var } # => 60
こうはなるよね
(1..10).inject(5) { |mem, var| mem.send(:+, var) } # => 60
Kernelのsendメソッドは
シンボル化されたメソッド名を第1引数に取れるんだ
ブロックをブロックとしてではなく
injectの引数として何とか渡したいなあ
ならブロックをオブジェクト化すればいいんだ
(1..10).inject(5, &lambda { |mem, var| mem.send(:+, var) }) # => 60
メソッドはその引数としてオブジェクトしか受け付けないけど
lambdaでブロックを手続きオブジェクトに変えてやれば
他のオブジェクトと同じようにカッコに入れられる
それで&(アンパサンド)を前置すれば
呼び出し側(injectメソッド内部)ではブロックに戻されて
ブロックとして評価されるようになる
さて次にこの手続きオブジェクトをどこかに隠したいなあ
そうか
シンボルのメソッドにしちゃえばいいんだよ!
つまりシンボルを
この手続きオブジェクトに変換するメソッドを書けばいいんだ
class Symbol def to_proc lambda { |mem, var| mem.send(:+, var) } end end (1..10).inject(5, &:+.to_proc) # => 60
すごいな俺!
これでto_procが取れたら完成なんだけど…
class Symbol def to_proc lambda { |mem, var| mem.send(:+, var) } end end (1..10).inject(5, &:+) # => 60
あれ?
取ってもうまくいくぞ
なんで?
そうか暗黙の型変換だよ
&を前置したからRubyは
それが手続きオブジェクトであると期待したんだ
でもその期待に反して&を伴っていたのはシンボルだったので
そのオブジェクトに手続きオブジェクトへの変換を要求
つまりto_procメソッドを自動で送信したんだ
できちゃったよ!
ラッキーだな俺!
じゃあ次にmapについても
同じように考えてみよう
基の式はこうだから
["ruby", "c", "lisp", "smalltalk"].map { |item| item.capitalize } # => ["Ruby", "C", "Lisp", "Smalltalk"]
こうはなるよね
["ruby", "c", "lisp", "smalltalk"].map { |item| item.send(:capitalize) } # => ["Ruby", "C", "Lisp", "Smalltalk"]
次にブロックをオブジェクト化する
["ruby", "c", "lisp", "smalltalk"].map(&lambda { |item| item.send(:capitalize) }) # => ["Ruby", "C", "Lisp", "Smalltalk"]
それでこの手続きオブジェクトをシンボルのto_procメソッドにすれば完成だ
class Symbol def to_proc lambda { |item| item.send(:capitalize) } end end ["ruby", "c", "lisp", "smalltalk"].map(&:capitalize) # => ["Ruby", "C", "Lisp", "Smalltalk"]
よし!
最後はこのto_procメソッドを一般化しなけりゃ
つまり上の2つの例ではそれぞれのメソッド:+と:capitalizeが
to_procに書かれてしまっている
これらはto_procメソッドの呼びだし元
つまりselfだからこれに置き換えよう
class Symbol def to_proc lambda { |obj, *args| obj.send(self, *args) } end end
うまいことにRubyのブロックはクロージャとして
外部環境を一緒に閉じ込めるから
to_proc内のブロックにおけるselfは
その呼びだし元(先の例では:+, :capitalize)となる
これでSymbol#to_procの完成だ!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
ってか
こういうことを考えつく人は
パッと閃いてサッと書いてしまうんでしょう…
僕はSymbol#to_procをこうやってやっと理解できたのでした
ただここまでくると
こんどはちょっと&が邪魔に思えてくる
だからやっぱりこう書きたい
(1..10).inject(5, :+) # => 60
あれ?
実行できる…
これは?
(1..10).inject(5, '+') # => 60
これもOKだ
じゃあmapも?
["ruby", "c", "lisp", "smalltalk"].map(:capitalize) # =>ArgumentError: wrong number of arguments (1 for 0)
mapはだめだった
Ruby1.9のリファレンスマニュアルを調べてみると…
injectメソッドはシンボルを渡すと
それをメソッドとして呼ぶように実装されていた*1
こうなるとmapもなんとかしたい
まずmapの実装等価コードを
mappメソッドとして書いてみる
module Enumerable def mapp i = 0 while i < self.length self[i] = yield self[i] i += 1 end self end end
こんな感じだろうか
["ruby", "c", "lisp", "smalltalk"].mapp(&:capitalize) # => ["Ruby", "C", "Lisp", "Smalltalk"] ["ruby", "c", "lisp", "smalltalk"].mapp { |item| item.capitalize } # => ["Ruby", "C", "Lisp", "Smalltalk"]
いいみたいだ
次にブロックが渡されないときの処理を分岐して
その場合には渡された第1引数をメソッドとして呼ぶようにしてみる
module Enumerable def mapp(*args) i = 0 while i < self.length self[i] = block_given? ? yield(self[i]) : self[i].send(args[0]) i += 1 end self end end
ブロックの有無の判断にはblock_given?メソッドを使う
["ruby", "c", "lisp", "smalltalk"].mapp(:capitalize) # => ["Ruby", "C", "Lisp", "Smalltalk"] ["ruby", "c", "lisp", "smalltalk"].mapp('capitalize') # => ["Ruby", "C", "Lisp", "Smalltalk"] ["ruby", "c", "lisp", "smalltalk"].mapp(&:capitalize) # => ["Ruby", "C", "Lisp", "Smalltalk"] ["ruby", "c", "lisp", "smalltalk"].mapp { |item| item.capitalize } # => ["Ruby", "C", "Lisp", "Smalltalk"]
うまくいった
ただどういうわけか
mapを再定義して上記を実装するとうまくいかない
自分の理解の限界に来たのでここまでとします