【ネタ】Rubyでインクリメント可能な整数オブジェクトを作ってみた

完成図

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 クラスの仕様をいろいろ勉強できたので、案外ネタコーディングも悪くないかも知れない。
まあ、今回得た知識が実務で役立つ場面は無いような気もするが。

最後に注意

※ 念のため言っておきますが、この記事はあくまでネタ、ジョークなので、間違っても実用のプログラムにこんなクラスを仕込んではいけませんよ?

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.