Rubyで文字列と数値が足し算出来ない理由


## はじめに

某所で「数値と文字列の足し算でエラー起きてた」というものを見かけた。
確かに数あるLLの中には暗黙的に型変換を行ってどうにかエラーにならないようにしているものがある。

```php:phpの場合
echo "1" + 2; # => 3
echo 1 + "2"; # => 3
```

```perl:perlの場合
print "1" + 2; # => 3
print 1 + "2"; # => 3
````

```javascript:JavaScript(nodejs)の場合
console.log("1" + 2); // => "12"
console.log(1 + "2"); // => "12"
```

とりあえずすぐに試せるもので試してみたが、挙動は異なるにせよ動作する。
では、Rubyではなぜ動かない、動くように実装されていないのだろうか。

## 文字列 + 数値の場合

この場合、`String#+`メソッドが呼ばれている。
`String#+`の実装である[string.cのrb_str_plus](https://github.com/ruby/ruby/blob/trunk/string.c#L1685)を読んでみよう。

```c
VALUE
rb_str_plus(VALUE str1, VALUE str2)
{
    VALUE str3;
    rb_encoding *enc;
    char *ptr1, *ptr2, *ptr3;
    long len1, len2;

    StringValue(str2);
    enc = rb_enc_check_str(str1, str2);
    RSTRING_GETMEM(str1, ptr1, len1);
    RSTRING_GETMEM(str2, ptr2, len2);
    str3 = rb_str_new(0, len1+len2);
    ptr3 = RSTRING_PTR(str3);
    memcpy(ptr3, ptr1, len1);
    memcpy(ptr3+len1, ptr2, len2);
    TERM_FILL(&ptr3[len1+len2], rb_enc_mbminlen(enc));

    FL_SET_RAW(str3, OBJ_TAINTED_RAW(str1) | OBJ_TAINTED_RAW(str2));
    ENCODING_CODERANGE_SET(str3, rb_enc_to_index(enc),
			   ENC_CODERANGE_AND(ENC_CODERANGE(str1), ENC_CODERANGE(str2)));
    RB_GC_GUARD(str1);
    RB_GC_GUARD(str2);
    return str3;
}
```

初っ端で`StringValue(str2)`としているが、この`StringValue`が何者か調べてみると[るりまにページがあった](http://docs.ruby-lang.org/ja/2.3.0/function/StringValue.html)。

> val が String でなければ to_str メソッドを使って String に変換します。 

つまり、数値のほうに`to_str`メソッドがあれば動作するはずだ。
るりまで`to_str`について調べてみたら[`Object#to_str`](http://docs.ruby-lang.org/ja/2.0.0/method/Object/i/to_str.html)が見つかった。
該当ページの説明には以下のような注意書きがある。

>  このメソッドを定義する条件は、
>
>   -  文字列が使われるすべての場面で代置可能であるような、
>   -  文字列そのものとみなせるようなもの
>
> という厳しいものになっています。 

各主要数値クラスはこの条件に合致していないと判断されたのだろう。
そのため`to_str`が実装されておらず、結果として`String#+`メソッドでエラーとなる。

どうしても`String#+`に数値を渡したい場合にどのようにすべきか考えてみた。
`to_s`ならば主要数値クラスには実装されているのでこうすればとりあえずは動く。
(Numeric, Integerクラスには`to_s`メソッドがないが、これらは抽象クラスなので気にしないことにする）

```ruby
class Numeric
  def to_str
    self.to_s
  end
end

p "1" + 2 # => "12"
p "1" + 1.234 # => "11.234"
```

PHPやPerl的な挙動(selfを数値に変換して足し算する)にしたい方も多いであろうが、`String#+`は四則演算の足し算を行うメソッドではなく、文字列の連結メソッドである。
そのためJavaScript的な挙動にするほうがRuby的には正しいであろう。
もしどうしてもselfを数値に変換して足し算したい場合は下記のようにすればよい。

```ruby
class String
  alias orig_plus +
  def + other
    if other.kind_of? Integer
      return self.to_i + other
    elsif other.kind_of? Float
      return self.to_f + other
    elsif other.kind_of? Rational
      return self.to_r + other
    elsif other.kind_of? Complex
      return self.to_c + other
    else
      orig_plus other
    end
  end
end
```

元々の`String#+`を`String#orig_plus`に退避し、各数値クラスに合わせてselfを変換すると動作する。
なお、この場合同様に`String#-`、`String#*`、`String#%`をそれぞれの演算に合わせて実装すべきであるが、`String#*`の引数は整数であるため元の`String#*`を捨てることになるし、`String#%`も引数は整数の場合もあるため元の`String#%`も捨てることになる。

## 数値 + 文字列の場合

この場合、各数値クラスの`+`メソッドが呼ばれている。
例として`Fixnum#+`の実装である[numeric.cのfix_plus](https://github.com/ruby/ruby/blob/ruby_1_9_3/numeric.c#L2399)を読んでみよう。

```c
static VALUE
fix_plus(VALUE x, VALUE y)
{
    if (FIXNUM_P(y)) {
	long a, b, c;
	VALUE r;

	a = FIX2LONG(x);
	b = FIX2LONG(y);
	c = a + b;
	r = LONG2NUM(c);

	return r;
    }
    switch (TYPE(y)) {
      case T_BIGNUM:
	return rb_big_plus(y, x);
      case T_FLOAT:
	return DBL2NUM((double)FIX2LONG(x) + RFLOAT_VALUE(y));
      default:
	return rb_num_coerce_bin(x, y, '+');
    }
}
```

otherが`Fixnum`でも`Bignum`でも`Float`でもない場合、`rb_num_coerce_bin`が呼ばれている。
ここから先は実際にnumeric.cを読んでいただきたいが、最終的にはotherの`coerce`メソッドをコールしている。
るりまに[`Numeric#coerce`](http://docs.ruby-lang.org/ja/2.3.0/method/Numeric/i/coerce.html)のページがあるのでそちらを読んでみる。

> 自身と other が同じクラスになるよう、自身か other を変換し [other, self] という配列にして返します。 

`Object#to_str`ほどの注意書きはないが、`Numeric`のサブクラスは実装すべきとしている。
`String`は当然`Numeric`のサブクラスではないため`coerce`が実装されておらず、結果として`+`メソッドでエラーとなる。

どうしても各主要クラスの`+`メソッドに文字列を渡したい場合、`String#coerce`があれば動作するはずだ。

```ruby
class String
  def coerce other
  if other.kind_of? Integer
      return [other, self.to_i]
    elsif other.kind_of? Float
      return [other, self.to_f]
    elsif other.kind_of? Rational
      return [other, self.to_r]
    elsif other.kind_of? Complex
      return [other, self.to_c]
    else
      [other.to_f, self.to_f]
    end
  end
end

p 1 + "2" # => 3
p 1 + "a" # => 1
p 1.234 + "1.234" #=> 2.468
```

デフォルトではself,other共に`Float`に変換するそうなので、主要数値クラスに当てはまらなければ`to_f`するようにしてみた。

## 終わりに

`"1" + 2`は明確なポリシーを持ってエラーとしていたが、`1 + "2"`は理由としてはいまひとつピンと来なかった。
が、`String`は当然数値を表すクラスではないため、四則演算の対象に含まれないのは当然と言えば当然である。

代替案として`String`や`Numeric`にモンキーパッチを当てるコードを提示しているが、このパッチを当てることを推奨している訳ではない。
仮に使うとしても[`Module#refine`](http://docs.ruby-lang.org/ja/2.3.0/method/Module/i/refine.html)を利用するなどして限られた範囲で利用できるようにし、影響を最小限に留めるべきだろう。