Python では数学のように 1 <= x < 5 のような比較式を書くことができます1が、それをうらやましく思うことがあります。…ええ、もちろん 1 <= x && x < 5 と書けばいいだけの話です2し、あるいは (1...5).include?(x) のように書いた方が Ruby 的ですね。でも、時には Ruby 的にではなくて 数学的に書きたいんですよ!
というわけで、Ruby の黒魔術を使って 1 <= x < 5 のような比較式を書けるようにしてしまおう、というあまり実用性の無い記事です。
そもそも、なぜ 1 <= x < 5 はエラーになるのか
さて、irb を起動して「1 < 2 < 3」とでも入力してみましょう。
irb(main):001:0> 1 < 2 < 3
NoMethodError (undefined method `<' for true:TrueClass)
はい、当然エラーになりました。ですがどうしてエラーになってしまったのでしょう?「それが Ruby の言語仕様なのだから当たり前だ」と思わずに、改めて考えてみます。
1 < 2 < 3 という式を与えられた Ruby は、まずは 1 < 2 の部分を計算します。これは実際には 1.<(2) というメソッド呼び出しであり、true が返ってきます。つまり、1 < 2 < 3 という式はまず true < 3 (つまりは true.<(3) )になるわけです。
ところが true には < というメソッドは定義されていないので、上述のように NoMethodError になってしまったわけですね。
では、1.<(2) の戻り値が true ではなく 2 になればどうでしょう。先ほど同様にまずは 1 < 2 < 3 の 1 < 2 の部分が計算され、戻り値が 2 なので 2 < 3 になります。つまり 1 < 2 と 2 < 3 の2つの比較が行われることになり、1 < 2 && 2 < 3 と書いた場合と実質同じ比較がなされたことになります。
つまり < が本来 true を戻す状況では右辺値を戻すように改造すれば、目的の式を書くことができそうです。
真の場合に < が右辺値を戻すように改造
さて、黒魔術を使っていきましょう。黒魔術師と言うと社会のルールから外れた無法者のイメージがありますが、現代の Ruby 黒魔術師はマナーをちゃんと守る必要があります。
というわけで組み込みクラスを改造するような場合には、Refinement を用いるのがマナーですね。
るりまサーチで「<=」と検索すると、<= を定義している組み込みクラス・モジュールは Module, Comparable, Hash, Integer, Float の 5 つのようですが、Module と Hash はあまり必要性が無い気がする3ので、他の Integer, Float, Comparable のクラス・モジュールの < 等を改造していきます。
module ContinuousComparison
refine Integer do
def <(other); super && other; end
def <=(other); super && other; end
def >(other); super && other; end
def >=(other); super && other; end
end
refine Float do
def <(other); super && other; end
def <=(other); super && other; end
def >(other); super && other; end
def >=(other); super && other; end
end
refine Comparable do
def <(other); super && other; end
def <=(other); super && other; end
def >(other); super && other; end
def >=(other); super && other; end
end
end
既存の比較式への影響は?
Refinement を使ったとはいえ基幹部分の改造ですので、既存のコードが正しく動かなくなりはしないか心配ですね。ですが、この改造ではそれほど影響は無いはずです。なぜなら < の戻り値自体は変化するものの、その真偽は変わらないからです。
- < がもともと false を戻す状況
- 変わらず、false が戻る。
- < がもともと true を戻す状況
- 右辺値が戻る。右辺値が偽 (false, nil) の場合は、そもそもエラーになるのでこの状況に該当しない。よって右辺値は必ず真である。
他の言語では 0 を偽として扱うことが多いですが、Ruby の「false と nil 以外はすべて真」という仕様のおかげで、こうした真似が可能になっています。
FalseClass を改造する
これで目的の式が書ける!…と言いたいところですが、下の式は NoMethodError になります。2 < 1 が false になり、false.<(3) になってしまうためです。
2 < 1 < 3
これを直すには、FalseClass を改造して < メソッドを付け加える必要があります。このメソッドは、いかなる状況でも false を返しておけば大丈夫でしょう。
refine FalseClass do
def <(other); false; end
def <=(other); false; end
def >(other); false; end
def >=(other); false; end
end
出来上がり
Refinement を使った黒魔術を発動するためには、呪文 using を詠唱する必要があります。
using ContinuousComparison
1 < 2 < 3 # => 3 (truely)
2 < 1 < 3 # => false
使う人がいるかはわかりませんが、一応 gem 化しました。
== は改造しないの?
上のコードを見て「不等号は改造したのに、等号は改造しないの?」と疑問に思った方もいるかもしれません。確かに、例えば「x と y の値が等しくて、どちらも正」を x == y > 0 と書けた方が嬉しそうです。
ところがこれについては2つ問題があったため、行いませんでした。
問題点1: a == b == c は SyntaxError になる。
a < b < c は Ruby の文法上問題無い式ですが、a == b == c は SyntaxError になってしまいます。
これは、演算子には「左結合」「右結合」「非結合」があり、< は左結合であるが == は非結合であることが理由です。(@znz さんに ruby-jp 上で教えていただきました。ありがとうございます。)
- 左結合: 例)
1 - 2 - 3は、(1 - 2) - 3と解釈され、-4 になる。 - 右結合: 例)
2 ** 2 ** 3は、2 ** (2 ** 3)と解釈され、256 になる。 - 非結合: 例)
1 == 2 == 3は、パースエラーとなる。
参考:第9章 速習yacc
ただ、どうして < と == で結合ルールに違いを持たせたのかは謎として残りました。4
ともかく、a == b == c と書くことはできません。(a == b) == c ならエラーにはなりませんが、そんな書き方するくらいなら a == b && b == c でいいです。
問題点2: < と == では演算子の優先度が違う。
また、仮に == も true 時に右辺値を戻すように改造したとします。その場合下のコードは真偽どちらになるでしょうか?
1 == 0 < 1
「偽に決まっているじゃないか」と思われた方も多いでしょうが、正解は真です。なぜなら == より < の方が優先度が高いので、1 == 0 < 1 は 1 == (0 < 1) と解釈され、< が右辺値を戻すよう改造されたため 1 == 1 となり、真になるのです。
このように予期しない結果になることがあるため、== までは改造しないほうが良さそうです。
ちなみに
-w オプション付きで実行すると、comparison '<' after comparison と警告が出ます。
Warning.warn を書き換えて警告が出ないようにしようとしたのですが、そもそもパースの時点で警告が出ているようなので、-r オプションでパース前に require するとかしない限り警告を抑えることはできないようです。