Ruby で Smalltalk の #with:collect:


rubyco(るびこ)の日記 - zipWithの自作もち - 今日の自作関数 にて、RubyHaskell の zipWith を自作しよう…という話。


Haskell の zipWith と似たような動きをするメソッドを Smalltalk で探すと、#with:collect: が見つかります。Squeak システムでは ArrayedCollection 版と OrderedCollection 版があり、それぞれ、こんな定義になっています。

ArrayedCollection >> with: otherCollection collect: twoArgBlock 
   | result |
   otherCollection size = self size ifFalse: [self error: 'otherCollection must be the same size'].
   result := self species new: self size.
   1 to: self size do:
      [:index | result at: index put:
      (twoArgBlock
         value: (self at: index)
         value: (otherCollection at: index))].
   ^ result
OrderedCollection >> with: otherCollection collect: twoArgBlock 
   | result |
   otherCollection size = self size ifFalse: [self error: 'otherCollection must be the same size'].
   result := self species new: self size.
   1 to: self size do:
      [:index | result addLast:
      (twoArgBlock
         value: (self at: index)
         value: (otherCollection at: index))].
   ^ result

使い方。

#(1 2 3) with: #(4 5 6) collect: [:aa :bb | aa + bb]   " => #(5 7 9) "


Ruby の an Array 挙動は、Smalltalk では、どちらかというと an OrderedCollection のほうに近いので、OrderedCollection 版のほうを直訳かつ端折り気味に書くと…、

module Enumerable
  def with_collect(other)
    result = []
    (0..size-1).each{|i| result << yield(self[i],other[i])}
    result
  end
end
>> [1,2,3].with_collect([4,5,6]){|a,b| a+b}
=> [5, 7, 9]


ただ、Ruby にはすでに zip があるので、次のように書いたほうが zip 同様3つ以上の組み合わせにも対応できて、よりクールかもしれませんね。

module Enumerable
  def zip_with(*others)
    result = []
    zip(*others){|ary| result << yield(ary)}
    result
  end
end
>> [1,2,3].zip_with([4,5,6]){|a,b| a+b}
=> [5, 7, 9]
>> [1,2,3].zip_with([4,5,6],[7,8,9]){|a,b,c| a+b+c}
=> [12, 15, 18]


関連:

このアイデアを取り入れれば、より zipWith に近づけることが可能かもしれません。


追記
せっかくなので試してみました。ひとまず、Symbol#to_proc を定義します。

class Symbol
  def to_proc
    Proc.new{|obj,*args| obj.send(self, *args)}
  end
end


これで、通常はブロックで記述して指定するメソッドを、代わりにシンボルだけで呼び出すことが可能になります。

>> [1,2,3].zip_with([4,5,6],&:+)
=> [5, 7, 9]
>> [1,2,3].zip_with([4,5,6],&:*)
=> [4, 10, 18]


ただ、ブロック引数に値を引き渡す際の Ruby 独特の挙動から、次のようなサンプルはうまく動きません。Symbol#to_proc の定義をもう少し、凝ったものにする必要があるでしょう。

module Enumerable
  def sum
    inject(0){|s,e| s+e}
  end
end
>> [1,2,3].zip_with([4,5,6],[7,8,9],&:sum)
NoMethodError: undefined method `sum' for 1:Fixnum
        from (irb):56:in `send'
        from (irb):56:in `to_proc'
        from (irb):9:in `zip_with'
        from (irb):9:in `zip_with'
        from (irb):62
        from :0


もちろん、きちん(?)としたブロック変数を持つブロック付きで呼び出せば問題はありません。念のため。

>> [1,2,3].zip_with([4,5,6],[7,8,9]){|ary| ary.sum}
=> [12, 15, 18]

追々記:
せっかくなので、&:sum でも使える Symbol#to_proc の定義を考えてみました。

class Symbol
  def to_proc
    Proc.new do |*args|
      if args.size > 1
        args.shift.send(self,*args)
      elsif (args = args.first).is_a?(Array) &&
          args.first.methods(true).include?(self.to_s) &&
          args.first.method(self).arity == args.size-1
        args.shift.send(self,*args)
      else
        args.send(self)
      end
    end
  end
end
>> [[1,2],[3,4],[5,6]].collect(&:+)
=> [3, 7, 11]
>> [[1,2],[3,4],[5,6]].collect(&:sum)
=> [3, 7, 11]
>> [1,2,3].zip_with([4,5,6],&:+)
=> [5, 7, 9]
>> [1,2,3].zip_with([4,5,6],&:sum)
=> [5, 7, 9]
>> [1,2,3].zip_with([4,5,6],[7,8,9],&:sum)
=> [12, 15, 18]