Numericクラスのinteger?とreal?の使われ方と返すべき値

Numeric.integer?

Numeric.integer?とは

自身が Integer かそのサブクラスのインスタンスの場合にtrue を返します。そうでない場合に false を返します。
Numeric のサブクラスは、このメソッドを適切に再定義しなければなりません。

https://docs.ruby-lang.org/ja/2.6.0/method/Numeric/i/integer=3f.html
Numeric.integer?の使われている箇所

Rubyのソース内を見た感じ、range.cの以下箇所で使われている。

static VALUE range_bsearch(VALUE range)

Rangeの始まりのオブジェクトと終わりのオブジェクトのinteger?がtrueを返す場合にto_intされてIntegerに変換されbsearchの処理がされている。

Numeric.integer?が返すべき値

クラスのオブジェクトが整数値としての表現が出来るか否か。
つまりto_intが正しく整数値を返すように実装されているか否か。
Numeric.to_intはNumeric.to_iを呼び出すので、Numericを直接継承した場合はto_iを実装するでも良い。

クラスごとに固定値を返すべきなのか、オブジェクトごとに変わって良いのかは、実装よりもRubyの思想による部分が大きいのでよくわからない。

実装の観点から言うと、クラス固定でももちろん動くし、オブジェクトごとに返す値が違っても動くし、オブジェクトの状態によって返す値が変わっても動く。
とりあえず現状Range.bsearchでしか使われていないから、小数や虚数が失われる事なくIntegerで表すことが出来るか、という観点でオブジェクトごとに返しても良いかもしれない。

思想の観点から言うと、Rubyで定義されているNumeric.integer?とInteger.integer?はクラスで固定値を返しているのでクラス固定が望ましいのかもしれない。
Numericを継承したクラスはfreezeしてimmutableであるべきで、そもそもオブジェクトの状態が変わるってのはナシかなと思う。

Integerかそのサブクラスのインスタンスかを判定するならkind_of?で良いのでは?は思想的にNG。
ダックタイピングなので、Integerとしての要件を満たすメソッドが定義されていればNumericやIntegerを継承していなくても、それはIntegerである。
Integerとしての動きをするか否かは、型で決めるのではなくオブジェクト自身が申告するべきという思想。

MyNumはIntegerとしての挙動をして、正しくRange.bsearchが動く。
ちなみに、MyNumはNumericを継承していなくても動く。

class MyNum < Numeric

  def initialize(value)
    @value = value
  end 

  def integer?
    true
    #false
  end 

  def to_i
    @value
  end 
  alias_method :to_int, :to_i

  def next
    self.class.new(@value + 1)
  end 
  alias_method :succ, :next

  def <=>(other)
    @value <=> other
  end 

  def coerce(other)
    [other, @value]
  end 
end

ary = [0, 4, 7, 10, 12] 
p (MyNum.new(1)...ary.size).bsearch {|i| ary[i] >= 4 } # => 1
p (1...MyNum.new(ary.size)).bsearch {|i| ary[i] >= 4 } # => 1
p (MyNum.new(1)...MyNum.new(ary.size)).bsearch {|i| ary[i] >= 4 } # => 1

MyNum.integer?がtrueを返す場合の実行結果。
内部ではIntegerとして処理されbsearchが実行出来る。

1
1
1

MyNum.integer?がfalseを返す場合の実行結果。
内部でInteger同士の比較が出来ずに例外が発生する。

Traceback (most recent call last):
	1: from mynum.rb:32:in `<main>'
mynum.rb:32:in `bsearch': can't do binary search for MyNum (TypeError)

Numeric.real?

Numeric.real?とは

常に true を返します。(Complex またはそのサブクラスではないことを意味します。)
Numeric のサブクラスは、このメソッドを適切に再定義しなければなりません。

https://docs.ruby-lang.org/ja/2.6.0/method/Numeric/i/real=3f.html
Numeric.real?の使われている箇所

complex.cの以下関数で使われている。

inline static void nucomp_real_check(VALUE num)
VALUE rb_complex_plus(VALUE self, VALUE other)
VALUE rb_complex_minus(VALUE self, VALUE other)
VALUE rb_complex_mul(VALUE self, VALUE other)
inline static VALUE f_divide(VALUE self, VALUE other, VALUE (*func)(VALUE, VALUE), ID id)
VALUE rb_complex_pow(VALUE self, VALUE other)
static VALUE nucomp_eqeq_p(VALUE self, VALUE other)
static VALUE nucomp_coerce(VALUE self, VALUE other)
static VALUE nucomp_convert(VALUE klass, VALUE a1, VALUE a2, int raise)

具体的に言うと、
・Complexと他のオブジェクトとの四則演算。(+, -, *, /, **)
・Complexと他のオブジェクトとの比較。(==)
・他のオブジェクトとComplexとの比較。(coerce)
・Complexオブジェクトを生成する際の引数チェック。

ダックタイピングどこいった

real?が使われている箇所は全て、Numericのサブクラスであるかも合わせてチェックされている。

if (k_numeric_p(other) && f_real_p(other)) {
  // 〜〜〜
}

Rubyで表すとこう。

if other.kind_of?(Numeric) && other.real?
  # 〜〜〜
end

Numericのサブクラスでありreal?がtrueを返せば、実数値に変換出来て四則演算が出来る、とメソッド以上の意味合いを持たせているように見える。
Numericの継承/実装とreal?の再定義/実装という言い方をすれば、ダックタイピングから外れて、Javaで言うところのinterface/abstract methodのような継承関係が機能を表すような作りになっていると言える。

Complexの四則演算

足し算を例にする。

/*
 * call-seq:
 *    cmp + numeric  ->  complex
 *
 * Performs addition.
 *
 *    Complex(2, 3)  + Complex(2, 3)   #=> (4+6i)
 *    Complex(900)   + Complex(1)      #=> (901+0i)
 *    Complex(-2, 9) + Complex(-9, 2)  #=> (-11+11i)
 *    Complex(9, 8)  + 4               #=> (13+8i)
 *    Complex(20, 9) + 9.8             #=> (29.8+9i)
 */
VALUE
rb_complex_plus(VALUE self, VALUE other)
{
    if (RB_TYPE_P(other, T_COMPLEX)) {
        VALUE real, imag;

        get_dat2(self, other);

        real = f_add(adat->real, bdat->real);
        imag = f_add(adat->imag, bdat->imag);

        return f_complex_new2(CLASS_OF(self), real, imag);
    }
    if (k_numeric_p(other) && f_real_p(other)) {
        get_dat1(self);

        return f_complex_new2(CLASS_OF(self),
                              f_add(dat->real, other), dat->imag);
    }
    return rb_num_coerce_bin(self, other, '+');
}

1つめの条件処理、otherがComplex Typeである場合。
Complex同士の足し算を行う。
実数同士、虚数同士を足し算して、Complex型のオブジェクトを返す。

2つめの条件処理、Numericのサブクラスでありreal?がtrueを返す場合。
otherを実数の数値として扱い足し算を行う。
Complexの実数とotherを足し算、Complexの虚数をそのまま。

いずれの条件にも当てはまらない場合。
otherがNumericではない場合や、other.real?がfalseを返す場合など。
other.coerce(self)が呼び出され同一のクラスに変換してから足し算を行う。

余談。
ここでいう「Complex Type」ってのはクラスとは違う、Cの構造体レベルの定義で、RComplexで表されるオブジェクトを言う。
include/ruby/ruby.hに定義されているruby_value_typeで識別され、実体はinternal.hに定義されてるRComplex。

enum ruby_value_type {
    // 〜〜〜
    RUBY_T_COMPLEX  = 0x0e,
    // 〜〜〜
};
struct RComplex {
    struct RBasic basic;
    VALUE real;
    VALUE imag;
};
Numeric.real?が返すべき値

明示的に以下のように呼ぶ事にする。
実数を持ち虚数を持たないNumericを継承したクラス => MyNum
実数も虚数も両方持つNumericを継承したクラス => MyComp

結論その1。(クラス固定値)
MyNum.real?はtrueを返すべき。
MyComp.real?はfalseを返すべき。

結論その2。(オブジェクト固定値)
MyCompが内包する虚数値が0だった場合trueを、0以外だった場合falseを返しても動く。
しかしその場合MyCompの虚数値によって、Complex + MyCompの戻り値の型が変わる事になる。
MyCompが実数値/虚数値以外の情報を保持いていなくて、Complexが返ってきてもMyCompが返ってきても以降の処理がダックタイピングでどうとでもなる、というなら気にしなくても良いと思うけどドキュメントは気持ち悪くなる。

以下ちぐはぐな実装。

MyComp.real?がtrueを返した場合、Complex + MyCompするとMyCompの虚数は失われる。
MyComp + Complexだと、MyComp.+の実装次第でどうにでも出来る。
しかしオペランドの順序が変わっただけで計算結果が異なるというのは宜しくないので、MyComp.real?はfalseを返すべき。

MyNum.real?がfalseを返した場合、MyNumをComplexに変換すれば虚数は失われずに計算出来るが気持ち悪い。
Complex + MyNumするとMyNum.coerceが呼ばれるのでそこで[Complex, Complex]を返せば虚数は失われずに計算出来る。
MyNum + ComplexするとMyNum.+が呼ばれるので、そこでComplex型のオブジェクトを返す、もしくはComplexを内包したMyNumを返す、とすれば第2オペランド虚数は失われずに計算出来る。
以上のようにComplexに依存した謎実装になってしまう。

クラスが固定値を返す例

以下2つのクラスを定義してComplexとの足し算をする。
・実数型MyNum2
複素数型MyComp

class MyNum2 < Numeric
  def initialize(value)
    @value = value
  end

  def +(other)
    MyNum2.new(@value + other)
  end

  def to_f
    @value.to_f
  end

  def real?
    true
  end

  def coerce(other)
    if other.kind_of?(Integer)
      [other.to_f, self.to_f]
    else
      super
    end
  end
end

class MyComp < Numeric
  attr_reader :real
  attr_reader :imag

  def initialize(real, imag=0)
    @real = real
    @imag = imag
  end

  def +(other)
    MyComp.new(self.real + other.real, self.imag + other.imag)
  end

  def real?
    false
  end

  def coerce(other)
    if other.kind_of?(Complex)
      [MyComp.new(other.real, other.imag), self]
    else
      super
    end
  end
end

p Complex(3, 1) + MyNum2.new(5)     # MyNum2は実数として、Complexの実数と足し算される。
                                    # MyNum2.coerceにComplex.realが渡され、[Float, Float]に変換しその足し算の結果が実数となる。
                                    # 返り値はComplex。
p MyNum2.new(5) + Complex(3, 1)     # MyNum2.+が呼ばれる。返り値はMyNum2。
puts

p Complex(3, 1) + MyComp.new(5, 2)  # MyComp.coerceが呼ばれ[MyComp, MyComp]に変換、MyComp.+が呼ばれる。
p MyComp.new(5,2 ) + Complex(3, 1)  # MyComp.+が呼ばれる。

実行結果。

(8.0+1i)
#<MyNum2:0x00007fa63f140540 @value=(8+1i)>

#<MyComp:0x00007fa63f140310 @real=8, @imag=3>
#<MyComp:0x00007fa63f1401a8 @real=8, @imag=3>

オブジェクトごとに固定値を返す例

MyComp2.imagが0だった場合trueを、0以外だった場合falseを返す複素数クラスMyComp2を定義して足し算をする。

class MyComp2 < Numeric
  attr_reader :real
  attr_reader :imag

  def initialize(real, imag=0)
    @real = real
    @imag = imag
  end

  def +(other)
    MyComp2.new(self.real + other.real, self.imag + other.imag)
  end

  def to_f
    @real.to_f
  end

  def real?
    @imag==0
  end

  def coerce(other)
    if other.kind_of?(Complex)
      [MyComp2.new(other.real, other.imag), self]
    else
      super
    end
  end
end

p Complex(3, 1) + MyComp2.new(5, 2) # クラスが固定値を返す例のMyCompと同じ。返り値はMyComp2。
p Complex(3, 1) + MyComp2.new(5, 0) # MyComp2は実数として扱われComplex.+が呼ばれるため、返り値はComplex。
puts

p MyComp2.new(5, 2) + Complex(3, 1) # クラスが固定値を返す例のMyCompと同じ。返り値はMyComp2。
p MyComp2.new(5, 0) + Complex(3, 1) # MyComp2.+が呼ばれるため、返り値はMyComp2。

実行結果。
返り値の型に注目。

#<MyComp2:0x00007fe84d98cf00 @real=8, @imag=3>
(8.0+1i)

#<MyComp2:0x00007fe84d98ccf8 @real=8, @imag=3>
#<MyComp2:0x00007fe84d98ca00 @real=8, @imag=1>

ちぐはぐな実装の例

class MyNum3 < Numeric
  def initialize(value)
    @value = value
  end

  def +(other)
    MyNum3.new(@value + other)
  end

  def real?
    false
  end

  def coerce(other)
    if other.kind_of?(Complex)
      [other, Complex(@value, 0)]
    else
      super
    end
  end
end

class MyNum4 < Numeric
  def initialize(value)
    @value = value
  end

  def +(other)
    Complex(@value + other)
  end

  def real?
    false
  end

  def coerce(other)
    if other.kind_of?(Complex)
      [other, Complex(@value, 0)]
    else
      super
    end
  end
end

class MyComp3 < Numeric
  attr_reader :real
  attr_reader :imag

  def initialize(real, imag=0)
    @real = real
    @imag = imag
  end

  def +(other)
    MyComp3.new(self.real + other.real, self.imag + other.imag)
  end

  def to_f
    @real.to_f
  end

  def real?
    true
  end

  def coerce(other)
    if other.kind_of?(Complex)
      [MyComp3.new(other.real, other.imag), self]
    else
      super
    end
  end
end

p Complex(3, 1) + MyNum3.new(5)     # MyNum3.coerceが呼ばれ[Complex, Complex]に変換され、Complex.+が呼ばれる。
p MyNum3.new(5) + Complex(3, 1)     # MyNum3.+が呼ばれ、Complexを内包するMyNum3が返る。
puts

p Complex(3, 1) + MyNum4.new(5)     # MyNum4.coerceが呼ばれ[Complex, Complex]に変換され、Complex.+が呼ばれる。
p MyNum4.new(5) + Complex(3, 1)     # MyNum4.+が呼ばれ、Complexが返る。
puts

p Complex(3, 1) + MyComp3.new(5, 2) # Complex.+が呼ばれ、MyComp3の虚数は失われ計算される。
p MyComp3.new(5, 2) + Complex(3, 1) # MyComp3.+が呼ばれ、虚数は失われずに計算される。

実行結果。
MyNum3とMyNum4は返り値の型に注目。
MyComp3は計算結果に注目。

(8+1i)
#<MyNum3:0x00007f9fcd0517b0 @value=(8+1i)>

(8+1i)
(8+1i)

(8.0+1i)
#<MyComp3:0x00007f9fcd050810 @real=8, @imag=3>

まとめ

Numeric.integer?は、小数や虚数、その他の付属情報を丸める事なくInteger値に変換出来るのであればtrueにしておけばいい。
falseを返しても特に困る事はなさそう。
中身がIntegerだろうがFloatだろうが、integer?がtrueを返すオブジェクトがほしいならto_iしてIntegerを返せば良いのでは?くらいには思う。

Numeric.real?がtrueを返す場合、Integer/Floatとの親和性がありComplexなど他から使われる型に適している。プリミティブ感強め。
Numeric.real?がfalseの場合、他の数値型を自身のクラスに変換して統制する前提でゴリゴリに作り込むのに適している。オブジェクト感強め。

なんでこんな事調べたか

平方根丸め誤差なしで計算出来るライブラリを作っていて。
ライブラリ自体は完成はしたけど、今回まとめた内容を元にreal?/integer?の扱いを変えるかも。

GitHub: https://github.com/yoshida-eth0/ruby-sqrt
RubyGems: https://rubygems.org/gems/hpsqrt

環境

$ ruby -v
ruby 2.7.0preview1 (2019-05-31 trunk c55db6aa271df4a689dc8eb0039c929bf6ed43ff) [x86_64-darwin18]