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
するとかしない限り警告を抑えることはできないようです。