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を再定義して上記を実装するとうまくいかない
自分の理解の限界に来たのでここまでとします


関連記事:Rubyのブロックはメソッドに対するメソッドのMix-inだ! - hp12c