LoginSignup
5
1

More than 3 years have passed since last update.

Ruby でも 1 <= x < 5 みたいに書きたい! ~やさしい黒魔術入門~

Last updated at Posted at 2020-12-12

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 < 31 < 2 の部分が計算され、戻り値が 2 なので 2 < 3 になります。つまり 1 < 22 < 3 の2つの比較が行われることになり、1 < 2 && 2 < 3 と書いた場合と実質同じ比較がなされたことになります。

つまり < が本来 true を戻す状況では右辺値を戻すように改造すれば、目的の式を書くことができそうです。

真の場合に < が右辺値を戻すように改造

さて、黒魔術を使っていきましょう。黒魔術師と言うと社会のルールから外れた無法者のイメージがありますが、現代の Ruby 黒魔術師はマナーをちゃんと守る必要があります。

というわけで組み込みクラスを改造するような場合には、Refinement を用いるのがマナーですね。

るりまサーチで「<=」と検索すると、<= を定義している組み込みクラス・モジュールは Module, Comparable, Hash, Integer, Float の 5 つのようですが、ModuleHash はあまり必要性が無い気がする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 < 11 == (0 < 1) と解釈され、< が右辺値を戻すよう改造されたため 1 == 1 となり、真になるのです。

このように予期しない結果になることがあるため、== までは改造しないほうが良さそうです。

ちなみに

-w オプション付きで実行すると、comparison '<' after comparison と警告が出ます。

Warning.warn を書き換えて警告が出ないようにしようとしたのですが、そもそもパースの時点で警告が出ているようなので、-r オプションでパース前に require するとかしない限り警告を抑えることはできないようです。


  1. Julia では Python よりもさらに数学的に、1 ≤ x < 5 と書けます。 

  2. ただし 1 <= x && x < 5 という書き方では、x が呼ぶたびに値の変わりうるようなメソッドだった場合に意図しない結果になり得ます。 

  3. 「このクラス X が A を継承していて、なおかつ B の先祖クラスかどうか知りたい! そしてそれを A < X < B と書きたい!」と今まで一度でも思ったことがある方が居ましたらご連絡ください。 

  4. 想像ですが、< を比較以外の用途に使うようなメソッドを作った場合の使い勝手を良くするためですかね? 

5
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1