モンモンブログ

技術的な話など

Rubyで2項演算子が単項演算子に化けて悪さする話

DateTime.now + 1.hourDateTime.now +1.hour は異なる値を返します。

何を言ってるのかわからねーと思うがこの実行結果を見てくれ。こいつをどう思う?

[10] pry(main)> DateTime.now + 1.hour
Fri, 29 Jan 2016 11:38:52 +0900         # こっちだと 11:38
[11] pry(main)> DateTime.now +1.hour
Fri, 29 Jan 2016 10:38:52 +0900         # こうすると 10:38。あれれ?

すごく・・・バグりそうです・・・///

なんでか

DateTime.now + 1.hour

DateTime.now() + 1.hour

と解釈されるのに対し、

DateTime.now +1.hour

DateTime.now(+1.hour)

と解釈されるためです。

前者では二項演算子だった + 演算子が、後者では単項演算子に化けてますね。

ちなみに Time.now の場合はこの問題は起こりません。Time.now は引数を取らないので ArgumentError となるため。

[15] pry(main)> Time.now +1.hour
ArgumentError: wrong number of arguments (given 1, expected 0)
from /Users/monmon/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/delorean-2.1.0/lib/delorean.rb:11:in `now'

ちなみにちなみに 1.hour は1時間分の秒数 = 3600 を返すもんだと思ってくれい。ActiveSupport が追加するメソッドだ。rails 使いなら常識だ。

どんな場合に起こるか?

以下のような場合に、この問題が起こるんだと思います。

  • 数値(っぽい)型のデフォルト値つきの引数1つを取り、数値(っぽい)型の値を返すメソッドがある
  • + または - 演算子の左側にこのメソッドの括弧なし呼び出し、右側に数値(っぽい値)が現れる
  • 演算子と右側の値の間にスペースがない

「数値っぽい」と表現したのは、+- 演算子で計算出来るものであれば多分なんでもよさげなので。

実験してみましょう。こんなメソッドを作って、

def hoge(n = 1)
  n
end

+ 演算子の前後のスペースを変えながら実行してみると、

hoge + 1    # 2 ✓
hoge+1      # 2 ✓
hoge+ 1     # 2 ✓
hoge +1     # 1 ×

このとおり、結果がおかしくなる場合がありました。

- 演算子でも同様です。

hoge - 1    # 0 ✓
hoge-1      # 0 ✓
hoge- 1     # 0 ✓
hoge -1     # -1 ×

こわいですね。

弊社サービスもこれが原因でバグってたことがあるのはみんなには内緒だよ。
雑なエンジニアは死滅すればいいのに。

教訓

スペースにもちゃんと気を配ってコード書きましょうねというお話でした。