この記事はPORT Advent Calendar8日目の記事(実質1日目)です。
業務には全く役に立たないRubyの話です。
概要
Rubyで1 < x < 100
みたいな構文で範囲チェックできるようにします。下記みたいな感じ。
paizaの実行環境準備しました(https://paiza.io/projects/YaFfzau3N6wrFmTs0W0YZQ)
x = 25
y = 75
if 1 < x < 50 < y < 100
print 'true'
end
# => true
はじめに
例えば、変数xの値が1から100の間に収まってるかを判定する(1 < x < 100
の判定)には、以下のようにするのが一般的かと思います。
x = 50
# 例1
x > 1 && x < 100 # => true
# 例2
x.between?(1, 100) # => true
でも、1 < x < 100
みたいに書ければ、それが一番直感的だし書きやすいですよね。
x = 50
# こんな書き方はできない
if 1 < x < 100 # x in 1 ~ 100
print "x in 1 ~ 100"
end
しかし当然ではありますが、このような書き方は標準のRubyでは使うことができません。
今回は、このような記法をRubyで無理やり実現させます。
実際のコード
こちらが、1 < x < 100
みたいな記法を実現させるためのコードの全貌です。詳細は以下で解説します。
コード(折りたたんであります)
{ '<' => [-1], '>' => [1], '<=' => [-1, 0], '>= ' => [1, 0]}.each do |ope, v|
[TrueClass, FalseClass].each do |klass|
klass.class_eval("def #{ope}(num); self ? value #{ope} num : false;end")
end
[Integer, Float].each do |klass|
klass.class_eval do
define_method(ope) do |n|
return false unless v.include? (self <=> n)
TrueClass.class_eval("def value; #{n};end")
true
end
end
end
end
解説
1. なぜこの構文が成立するのか
まず、通常のRubyで1 < x < 100
を実行してみます。
x = 50
1 < x < 100 # => NoMethodError: undefined method `<' for true:TrueClass
SyntaxError
ではなくNoMethodError
になりました。つまり、この構文はRubyでは使用可能だということです。
<
は.<
のシンタックスシュガーなので、1 < x < 100
は以下のように書き換えることができます。
1.<(x).<(100) # "1 < x < 100" と同じ
1.<(x)
の戻り値はtrue
になるので、Rubyはtrue.<(100)
を実行しようとします。しかし、true
に<
というインスタンスメソッドは存在しないので、NoMethodError
となるわけです。
2. true
に<
メソッドを追加する
存在しないなら作るまでです。true
インスタンスから<
を呼び出すために、<
メソッドを定義します。
TrueClass.class_eval do
def <(n)
n
end
end
これで、true < 100
と書けるようになりました。x = 50
の場合、1 < x
の戻り値はtrue
なので、1 < x < 100
も評価可能になります。
しかし、この段階では式の実行を可能にしただけで、値の比較はできていません。
3. Integer#<
の戻り値を改造する
先ほど定義したTrueClass#<
の中で値を比較するためには、1 < x
の戻り値であるtrue
からx
を取り出す必要があります。Integer#<
をオーバーライドして、これを実現可能にします。
Integer.class_eval do
def <(n)
if (self <=> n) == -1
TrueClass.class_eval("def value; #{n};end")
true
else
false
end
end
end
TrueClass.class_eval do
def <(n)
value < n
end
end
ここでは、TrueClass#value
メソッドを定義することで、<
で評価した右辺の値をtrue.value
でとり出せるようにしています。前述したTrueClass
のコードも、これに対応させて書き直してあります。
これで、1 < x < 100
がtrue
or false
を返すようになりました!
x = 50
1 < x < 100 # true
1 < x < 25 # false
ただし、このままだと1 < x
がfalseの場合に対応できていなかったりと様々な問題があるので、諸々の調節が必要です。
4. <
以外のオペレータへの対応・調整
ここまでで解説したコードに必要な調整を加え、<
、>
、<=
、>=
4つのオペレーターに対応させたコードが以下になります。
{ '<' => [-1], '>' => [1], '<=' => [-1, 0], '>= ' => [1, 0]}.each do |ope, v|
[TrueClass, FalseClass].each do |klass|
klass.class_eval("def #{ope}(num); self ? value #{ope} num : false;end")
end
[Integer, Float].each do |klass|
klass.class_eval do
define_method(ope) do |n|
return false unless v.include? (self <=> n)
TrueClass.class_eval("def value; #{n};end")
true
end
end
end
end
今回は、Integer
とFloat
にのみ対応させてあります。以下の式はいずれも正確に判定可能です。
1 < 3 <= 3 < 3.5 < 5 < 5.001 # => true
1 < 3 <= 3 < 3.5 < 5.1 < 5.001 # => false
100 > 40 <= 40 < 50 >= 1 > -100 # true
最後に
同様の手法で、Time
やDate
の範囲チェックもできるようにすれば、かなり便利になりそうです。ただし、この改造が及ぼす副作用に関してはあまり検証をしていないので、思わぬところで問題が発生するかもしれません(例えば、この改造がスレッドセーフなのかとか)。
最後になりますが、PORT株式会社では自社サービスを支えてくれる優秀なRubyエンジニアを募集しています(Rubyエンジニア以外も)。
興味のある方は、masa2229に連絡するといいかもしれません。
PORT Advent Calendarは明日以降も続きます。
明日は私の新卒メンターだった@masa2229が素晴らしい記事を書いてくれます。