Techioz Blog

Ruby でクラスのインスタンスの get メソッドを実装し、それでもオブジェクト メソッドにアクセスするにはどうすればよいですか?

概要

クラスのインスタンスを変数に書き込み、値の取得とメソッドへのアクセスの両方にアクセスできるようにする必要があります。これを何とか実装できないでしょうか?

例えば:

class A
  def initialize(value)
    @value = value
  end

  def preview
    puts "Class preview: #{@value}"
  end

  def something(param)
    puts "Something method: #{@value * param}"
  end
end

class B
  attr_reader :obj

  def set_object(obj)
    @obj = obj
  end
end

b = B.new
b.set_object(A.new(5))

b.obj # ==> 5
10 + b.obj # ==> 15

b.obj.preview # ==> "Class preview: 5"
b.obj.something(3) # ==> "Something method: 15"

解決策

コードの根本的な問題は、数値のように動作するオブジェクトを実装しているにもかかわらず、Numeric クラスによって規定されている数値のような型に関する Ruby のプロトコルに従っていないことです。

特に、Numeric#coerce メソッドによって提供される算術強制プロトコルが欠落しています。

Ruby では、算術演算子がオペランドをどう処理すればよいかわからない場合は常に、オペランドに強制メッセージを送信し、対処方法がわかっているオペランドのペアで応答するように指示します。

たとえば、+ メッセージを 10 に送信し、引数として A のインスタンスを渡すと、メソッド Integer#+ が呼び出されます。したがって、この行では次のようになります。

10 + b.obj

ここでは、整数リテラル 10 (の評価結果) にメッセージ + を送信し、式 b.obj (つまり A のインスタンス) の評価結果を引数として渡しています。

つまり、ここで得られるものは本質的に次のとおりです。

some_integer + some_a

ここで、問題は、もちろん、Integer#+ が A のインスタンスをそれ自体に追加する方法を知らないことです。ただし、すべての算術演算は算術強制プロトコルを観察します。つまり、Integer#+ の実装は次のようになります。

class Integer
  def +(other)
    if other.is_a?(Integer)
      # I know what to do!
      # Do whatever internal magic computes the sum of two `Integer`s
    else
      coerced_self, coerced_other = other.coerce(self)
      coerced_self + coerced_other
    end
  end
end

トリックがわかりますか? Integer#+ は、Integer と As を追加する方法を知りません。Integer#+ がこの時点でほぼ 30 年前に書かれているのに対し、あなたは今日 A を書いたばかりなので、これは驚くべきことではありません。ただし、Integer は標準の Ruby クラスであるため、A が整数の処理方法を知っていることが前提となっています。

したがって、ここで Integer#+ が行うことは、A の強制メソッドを呼び出して「あなたが何者なのか知りませんが、ここでは私自身を引数として渡しています。あなたが私が何であるかを知っていただければ幸いです。だから私自身とあなた自身を、その二つを足し合わせる方法を知っている何かに変えてください。」

つまり、A の強制メソッドを実装する必要があります。強制のプロトコルは次のとおりです。

そこで、強制を実装しましょう。

class A
  def coerce(other) = [other, @value]
end

ここで、質問内のコードを実行すると、次の結果が得られます。

#<A:0x0000000101021cd0 @value=5>
15
Class preview: 5
Something method: 15

ご覧のとおり、式 10 + b.obj は正しく 15 と評価されました。

コードに関する次の問題は、オブジェクトによって常にオーバーライドされるべき標準メソッドの一部をオーバーライドしていないことです。 BasicObject#==、Object#eql?、Object#hash、Object#to_s などのメソッドについて話しています。

特に、人間が判読できるオブジェクトのデバッグ表現を表示するために送信されるメッセージは検査されます。 Object#inspect をオーバーライドしていないため、クラスに関する情報、実装定義の識別子、およびインスタンス変数とその値のリストを含むデフォルトの実装を取得します。

次のように動作するには、inspect をオーバーライドする必要があります。

class A
  def inspect = @value.inspect
end

ここで、質問内のコードを実行すると、望ましい結果が得られます。

5
15
Class preview: 5
Something method: 15

コードには他にも改善できる点がいくつかあります。

これをすべてまとめると、次のようになります。

class A < Numeric
  include Comparable

  def initialize(value)
    super()
    @value = value
  end

  def to_int = @value
  alias to_i to_int

  def to_s = @value.to_s
  alias inspect to_s

  def +(other)
    case other
    when A
      self.class.new(@value + other.value)
    when Integer, Float, BigDecimal, Rational, Complex
      self.class.new(@value + other)
    else
      raise(TypeError, "Don't know how to add #{other.inspect} of class #{other.class}") unless other.respond_to?(:coerce)

      coerced_self, coerced_other = other.coerce(self)
      coerced_self + coerced_other
    end
  end

  def -(other)
    case other
    when A
      self.class.new(@value - other.value)
    when Integer, Float, BigDecimal, Rational, Complex
      self.class.new(@value - other)
    else
      raise(TypeError, "Don't know how to add #{other.inspect} of class #{other.class}") unless other.respond_to?(:coerce)

      coerced_self, coerced_other = other.coerce(self)
      coerced_self - coerced_other
    end
  end

  def *(other)
    case other
    when A
      self.class.new(@value * other.value)
    when Integer, Float, BigDecimal, Rational, Complex
      self.class.new(@value * other)
    else
      raise(TypeError, "Don't know how to add #{other.inspect} of class #{other.class}") unless other.respond_to?(:coerce)

      coerced_self, coerced_other = other.coerce(self)
      coerced_self * coerced_other
    end
  end

  def /(other)
    case other
    when A
      self.class.new(@value / other.value)
    when Integer, Float, BigDecimal, Rational, Complex
      self.class.new(@value / other)
    else
      raise(TypeError, "Don't know how to add #{other.inspect} of class #{other.class}") unless other.respond_to?(:coerce)

      coerced_self, coerced_other = other.coerce(self)
      coerced_self / coerced_other
    end
  end

  def coerce(other)
    case other
    when Integer, Float, BigDecimal, Rational, Complex
      [self.class.new(other), self]
    else
      [other, to_int]
    end
  end

  def <=>(other) = to_i <=> other.to_i

  def preview
    puts("Class preview: #{self}")
  end

  def something(param)
    puts("Something method: #{self * param}")
  end

  protected

  attr_reader(:value)
end

class B
  attr_reader :obj

  def initialize(obj)
    @obj = obj
  end
end