言う間でもなくRubyの Fixnum はイミュータブルで、破壊的変更を許すメソッドを持ち合わせていない。
result1 = %w(foo bar buz qiiz).each_with_object(0) do |str, num|
num += str.length
end
上記のようなコードは「0」を返す。Rubyのブロックには「オブジェクトの参照のコピー」が渡されるため、0自体を直接操作できない以上そうなる(何でこうなるかが分からない人はRubyを沢山勉強しよう!)。
でも、例えばeachでぶん回して操作成功した件数知りたい、とか言う場合、ぶっちゃけ破壊的操作できる数値オブジェクトが欲しくない?
書いてみた。
class NumericWrap < BasicObject
def initialize(n)
@num = n
end
def self.regard_as_destructive(*ops)
ops.each do |op|
define_method(op) do |other|
@num = @num.send(op, other)
self
end
end
end
# Picked up randomly...
regard_as_destructive :+, :-, :*, :/, :%, :**, :&, :|
def method_missing(*a)
@num.send(*a)
end
end
result1 = %w(foo bar buz qiiz).each_with_object(0) do |str, num|
num += str.length
end
result2 = %w(foo bar buz qiiz).each_with_object(NumericWrap.new(0)) do |str, num|
num += str.length
end
p result1
# => 0
p result2
# => 13
BasicObject を継承したクラスをラッパーにして、必要なメソッド以外は全部 method_missing で委譲するようなパターンを勝手に「Delegate-all pattern」と名付けたけど、他に良い名前がありそう(メタプログラミングRubyとかでBlank Slateという名前で同じようなものを説明してたような気がする)。
Object#class、Object#is_a?、Object#inspectとかも全部上書きされているため、見た目上まるっきりFixnumとして取り扱うことができてしまう。でも、 Fixnum と直接の継承関係はない 。
num = NumericWrap.new(0)
num.class #=> Fixnum
直感的に凄い不気味……。
Delegate-all patternは他には、ActiveSupport::Durationに使われている。というかこの実装を見てパクった。