Ruby で Smalltalk の #with:collect:
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))].
^ resultOrderedCollection >> 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]