String#succ問題への回答

String#succ で提起されている問題はかなり面白い。これをどう捉えるかが、数学屋と計算機屋で分かれそうだ。

Ruby的にスマートそうな回答は walf443さんが提出済み 。succに限らない一般化バージョンもある。

でも、私の目には、モノが"succ"だけにこれは関数合成の問題に見える。 "str".succ.succ(succ ∘ succ)("str") に見えるのね。今『 圏論の基礎 』に再挑戦してるから succ succ "str" と書こうか。

数学屋-計算機屋と書いたけれど、これは関数型言語系と手続き型言語系で分かれるといった方が正しいかもしれない。このあたり、関数型言語なら多変数/多価関数も含めてスマートに書けるんだろう。Haskellなんかsuccはまんま succ (succ "str") だもんね。

そういう視点からこの問題を捉えると、実際この手のメソッドチェーンはレシーバーを出発点とした射の合成な訳だよね。ここで、ActiveSupportに入った Symbol#to_proc を思い出す( くまくま参照 )。あんな感じで、シンボルを元に射の合成を表現できたらいいなぁ、と。

実装

で、書いてみました。

class Symbol
  unless instance_methods.include?("to_proc")
    def to_proc
      Proc.new{|obj, *args| obj.__send__(self, *args)}
    end
  end

  def * rhs
    MethodComposition.new([self.to_proc, rhs.to_proc])
  end

  def ** num
    MethodComposition.new(Array.new(num, self.to_proc))
  end
end

class MethodComposition
  def initialize(elements = [])
    @chain = elements
  end

  def * rhs
    MethodComposition.new(@chain.dup.push(rhs.to_proc))
  end

  def ** num
    MethodComposition.new(@chain * num)
  end

  def to_proc
    Proc.new{|receiver|
      @chain.inject(receiver) {|obj, proc| proc.call(obj) }
    }
  end
end

class Object
  def callcomp(proc = nil)
    unless proc
      self
    else
      proc.to_proc.call self
    end
  end
end

使いかた

例えば、

"b8".callcomp(:succ ** 5 * :succ * :succ ** 3) # => "c7"
1.callcomp(:succ ** 3 * :to_s)                 # => "4"

[64, 65, 66].map(&(:succ * :chr * :to_sym))    # => [:A, :B, :C]

感想

  • 効率は無視してProcオブジェクト化して格納
  • 関数合成だから、演算子は乗算と冪乗算を借りた

  • あと、Symbolに乗算を定義する意味は普通無さそうなので衝突しなそう

  • 名前が Object#callcomp なのは callcc からの連想だけれど、もっとまともな名前はないものかと自分でも思ってる

  • というか、 succ を数回程度ならかえって複雑にしてるだけ
  • でも、mapとかに適用すると少しだけありがた味が出てくるかも
  • 1変数(というか self )で1価の関数しか扱えないのがちょっと難かなぁ。