読者です 読者をやめる 読者になる 読者になる

Ruby の Enumerable#sum

最近のruby-core (2016年7月)」に次のような記述がありました。

Enumerable#sum というメソッドが追加されており、特定の場合(浮動小数点数の配列とか)には誤差が累積しないアルゴリズムが採用されています。

Ruby 2.4 に Enumerable#sum が追加されたのは知ってましたが、「誤差が累積しない」というのは知りませんでした。

というか、元々誤差が累積しない加算の目的で追加されたものだったのですね(最近のruby-core (2016年3月))。単純に 「#inject(:+)」にわかりやすい名前をつけただけなのかと思ってました。

簡単に試してみます。浮動小数点演算で、0.1を10個足すと1.0にならないというのはよく知られていますね。

> 0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1+0.1
=> 0.9999999999999999

> [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1].inject(:+)
=> 0.9999999999999999

> [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1].sum
=> 1.0

sum を使った場合はちゃんと 1.0 になりました!

軽く Ruby のソースを眺めてみたのですが、Array#sumEnumerable#sum とは別に実装されていました。 ロジックは同じようなのでおそらく高速化のためだと思います。

ソースのコメント中に次のように書かれていました。

Array#sum method may not respect method redefinition of "+" methods such as Fixnum#+.

数値クラスの + メソッドを上書きしても sum メソッドでは効かないようです。

sum メソッド中の数値の足し算は C レベルでやってるので Ruby で定義したメソッドが呼ばれないのですね。

「合計を返すとは言ったが + を使うとは言ってない!」( ー`дー´)キリッ

試してみます。

class Integer
  # a + b で a * b の結果を返す
  def +(other)
    self * other
  end
end
p 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1  #=> 1
p [1, 1, 1, 1, 1, 1, 1, 1, 1, 1].sum     #=> 10

sum メソッドのループの実装は、次のようになっているようです。

  1. Integer または Rational オブジェクトの間繰り返し。それ以外のオブジェクトが現れたら 2 へ。
  2. Integer, Rational, Float オブジェクトの間繰り返し(ここで誤差が累積しないアルゴリズムが使用される)。それ以外のオブジェクトが現れたら 3 へ。
  3. + メソッド呼び出しを繰り返し。

ということで、次のように数値オブジェクトではないものが配列にあると、それ以降は「誤差が累積しないアルゴリズム」は使われません。

class A
  # 最初に 0 + self が呼ばれるので 0.0 を返す
  def coerce(other)
    [other, 0.0]
  end
end

p [A.new, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1].sum  #=> 0.9999999999999999

まあ、わざわざこんなことする人がいるとは思えませんけど。