rubyco(るびこ)の日記 - zipWithの自作 と もち - 今日の自作関数 にて、Ruby で Haskell の 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]