完成図
x = MutableInteger.new(3)
x #=> 3
### 一見普通のIntegerっぽく振る舞う ###
x + 1 #=> 4
x - 2 #=> 1
x * 3 #=> 9
x.even? #=> false
### Integerっぽいのに破壊的に操作できる! ###
x.next! #=> 4
x.next! #=> 5
x #=> 5
### 前置インクリメントをサポート!! ###
++x #=> 6
++x #=> 7
x #=> 7
実装
#next!
メソッドの実装
ruby の数値オブジェクトは不変(immutable)なので、Integer
クラスを再オープンしても、破壊的なメソッドを実装することはできない。
class Integer
def next!
# ここに何を書いても、インクリメントを実装できない
end
end
そのため、 整数インスタンスをラップしたクラスを作り、すべてのメソッドを委譲(delegate)する という方法をとる。
class MutableInteger
def initialize(integer)
raise "引数は整数にしてね" unless integer.is_a?(Integer)
@component = integer
end
def next!
@component = @component.next
self
end
def to_i
@component
end
def method_missing(name, *args)
super unless @component.methods.include?(name)
@component.send(name, *args)
end
end
なお、#next!
メソッドで self を返しているのは、
x.next!.next!.next!
のような記述をサポートするためである。
さて、さっそく irb で動かしてみよう
x = MutableInteger.new(3)
#=> #<MutableInteger:0x00007fccef93b5c0 @component=3>
y = x + 2
#=> 5
x.even?
#=> false
x.next!
#=> #<MutableInteger:0x00007fccef93b5c0 @component=4>
x.even?
#=> true
概ね想定どおりの動きだが、出力が数値っぽくない。
ディスプレイ表示を数値っぽくする
#to_s
および #inspect
の結果が Integer
クラスと同じになるように修正する。
class MutableInteger
def initialize(integer)
raise "引数は整数にしてね" unless integer.is_a?(Integer)
@component = integer
end
def next!
@component = @component.next
self
end
def to_i
@component
end
+ def to_s(base = 10)
+ @component.to_s(base)
+ end
+ alias inspect to_s
def method_missing(name, *args)
super unless @component.methods.include?(name)
@component.send(name, *args)
end
end
今回のコーディング中に知ったのだが、Integer#to_s
メソッドは基数を引数に取れるらしい。
5.to_s(2) #=> "101"
デフォルトだと10進数に変換したいので、 base = 10
とデフォルト値を設定した。
これで表示が数値っぽくなった。
x = MutableInteger.new(3)
#=> 3
x.next!
#=> 4
x.to_s
#=> "4"
x.to_s(2)
#=> "100"
演算結果を MutableInteger
にする part1
ここまでの記述だけでは、四則演算などを挟むとクラスが Integer
に戻ってしまう。
x = MutableInteger.new(3)
y = x + 2
x.class #=> MutableInteger
y.class #=> Integer
y.next! #=> NoMethodError (undefined method `next!' for 5:Integer)
これを解決するため、メソッドの処理結果が Integer
だった場合、MutableInteger
に変換して返すように変更する。
def method_missing(name, *args)
super unless @component.methods.include?(name)
- @component.send(name, *args)
+ ret = @component.send(name, *args)
+ ret = MutableInteger.new(ret) if ret.is_a?(Integer)
+ ret
end
理想どおりに動くようになった
x = MutableInteger.new(3)
y = x + 2
x.class #=> MutableInteger
y.class #=> MutableInteger
y.next! #=> 6
演算結果を MutableInteger
にする part2
上記の対応を行っても、y = 2 + x
のような計算を行うと、y が Integer
になってしまう。
x = MutableInteger.new(3)
y = 2 + x
x.class #=> MutableInteger
y.class #=> Integer
y.next! #=> NoMethodError (undefined method `next!' for 5:Integer)
MutableInteger
を2項演算子の右側に置いた場合でも、MutableInteger
のインスタンスを返すように修正する。
具体的には、#coerce
メソッドをオーバーライドし、2項演算子の左側を MutableInteger
に変換してやれば良い。
class MutableInteger
# 中略
+ def coerce(other)
+ [MutableInteger.new(other), to_i]
+ end
# 中略
end
理想どおりに動くようになった
x = MutableInteger.new(3)
y = 2 + x
x.class #=> MutableInteger
y.class #=> MutableInteger
y.next! #=> 6
前置インクリメントの実装
rubyは単項演算子をオーバーライドできるので、それを利用する。
単項の +
が、「同じ行から2回」呼び出されたらインクリメントを行う。
class MutableInteger
def initialize(integer)
raise "引数は整数にしてね" unless integer.is_a?(Integer)
@component = integer
+ @callers = []
end
+ def +@
+ @callers << caller
+ if @callers[-1] == @callers[-2]
+ @callers = []
+ return next!
+ end
+
+ @component = +@component
+ self
+ end
# 中略
def method_missing(name, *args)
super unless @component.methods.include?(name.to_sym)
+ @callers = [] if name != :+@
ret = @component.send(name, *args)
ret = MutableInteger.new(ret) if ret.is_a?(Integer)
ret
end
end
同じ行からの呼び出しかどうか判定するため、 Kernel.caller
を利用した。(生まれて初めて使った)
https://docs.ruby-lang.org/ja/2.5.0/method/Kernel/m/caller.html
ちなみに、「同じ行から2回」がトリガーなので、
+x; +x
のような記述でもインクリメントされてしまうバグがある。
頑張ったが直せなかった。
完成!!
最終的に、以下のようなクラスとなった。
class MutableInteger
def initialize(integer)
raise "引数は整数にしてね" unless integer.is_a?(Integer)
@component = integer
@callers = []
end
def +@
@callers << caller
if @callers[-1] == @callers[-2]
@callers = []
return next!
end
@component = +@component
self
end
def coerce(other)
[MutableInteger.new(other), to_i]
end
def next!
@component = @component.next
self
end
def to_i
@component
end
def to_s(base = 10)
@component.to_s(base)
end
alias inspect to_s
def method_missing(name, *args)
super unless @component.methods.include?(name.to_sym)
@callers = [] if name != :+@
ret = @component.send(name, *args)
ret = MutableInteger.new(ret) if ret.is_a?(Integer)
ret
end
end
作った感想
単項演算子や #coerce
メソッドのオーバーライド、Kernel.caller
など、ふだん絶対に使わないものが使えたので楽しかった。出来上がったコードは、虚しく儚く、無足で無益な役立たずだが、その過程で Integer
クラスの仕様をいろいろ勉強できたので、案外ネタコーディングも悪くないかも知れない。
まあ、今回得た知識が実務で役立つ場面は無いような気もするが。
最後に注意
※ 念のため言っておきますが、この記事はあくまでネタ、ジョークなので、間違っても実用のプログラムにこんなクラスを仕込んではいけませんよ?