はじめに
某所で「数値と文字列の足し算でエラー起きてた」というものを見かけた。
確かに数あるLLの中には暗黙的に型変換を行ってどうにかエラーにならないようにしているものがある。
echo "1" + 2; # => 3
echo 1 + "2"; # => 3
print "1" + 2; # => 3
print 1 + "2"; # => 3
console.log("1" + 2); // => "12"
console.log(1 + "2"); // => "12"
とりあえずすぐに試せるもので試してみたが、挙動は異なるにせよ動作する。
では、Rubyではなぜ動かない、動くように実装されていないのだろうか。
文字列 + 数値の場合
この場合、String#+メソッドが呼ばれている。
String#+の実装であるstring.cのrb_str_plusを読んでみよう。
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が何者か調べてみるとるりまにページがあった。
val が String でなければ to_str メソッドを使って String に変換します。
つまり、数値のほうにto_strメソッドがあれば動作するはずだ。
るりまでto_strについて調べてみたらObject#to_strが見つかった。
該当ページの説明には以下のような注意書きがある。
このメソッドを定義する条件は、
- 文字列が使われるすべての場面で代置可能であるような、
- 文字列そのものとみなせるようなもの
という厳しいものになっています。
各主要数値クラスはこの条件に合致していないと判断されたのだろう。
そのためto_strが実装されておらず、結果としてString#+メソッドでエラーとなる。
どうしてもString#+に数値を渡したい場合にどのようにすべきか考えてみた。
to_sならば主要数値クラスには実装されているのでこうすればとりあえずは動く。
(Numeric, Integerクラスにはto_sメソッドがないが、これらは抽象クラスなので気にしないことにする)
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を数値に変換して足し算したい場合は下記のようにすればよい。
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を読んでみよう。
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のページがあるのでそちらを読んでみる。
自身と other が同じクラスになるよう、自身か other を変換し [other, self] という配列にして返します。
Object#to_strほどの注意書きはないが、Numericのサブクラスは実装すべきとしている。
Stringは当然Numericのサブクラスではないためcoerceが実装されておらず、結果として+メソッドでエラーとなる。
どうしても各主要クラスの+メソッドに文字列を渡したい場合、String#coerceがあれば動作するはずだ。
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を利用するなどして限られた範囲で利用できるようにし、影響を最小限に留めるべきだろう。