I need to write an instance of a class to a variable and be able to access it both to get its value and to access its methods. Can I implement this somehow?
For example:
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"
The fundamental problem with your code is that you are implementing an object that behaves like a number, but without following Ruby's protocol for number-like types, as laid out by the Numeric
class.
In particular, you are missing the arithmetic coercion protocol provided by the Numeric#coerce
method.
In Ruby, whenever an arithmetic operator does not know what to do with its operand, it will send the operand the coerce
message and tell it to respond with a pair of operands it does know how to deal with.
For example, when send the +
message to 10
and pass an instance of A
as an argument, the method Integer#+
will be invoked. So, in this line:
10 + b.obj
Here, you are sending the message +
to the (result of evaluating the) Integer
literal 10
, and passing the result of evaluating the expression b.obj
(i.e., an instance of A
) as an argument.
So, what we have here is essentially:
some_integer + some_a
Now, the problem is, of course, that Integer#+
doesn't know how to add an instance of A
to itself. However, every arithmetic operation observes the arithmetic coercion protocol, i.e., the implementation of Integer#+
looks a little bit like this:
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
Do you see the trick? Integer#+
does not know how to add Integer
s and A
s, which is not surprising since you just wrote A
today whereas Integer#+
was written almost 30 years ago at this point. However, since Integer
is a standard Ruby class, it assumes that A
knows how to deal with Integer
s!
So, what Integer#+
does here, is that it calls A
's coerce
method and says "Hey, I don't know what you are, but here I am passing myself as the argument, and I hope that you know what I am, so please convert myself and yourself to something that does know how to add the two together".
Which means, we need to implement a coerce
method for A
. The protocol for coerce
is as follows:
coerce
gets sent to the right operand of the arithmetic operation with one argument, which is the left operand of the arithmetic operation.coerce
needs to return a pair of objects [coerced_left_operand, coerced_right_operand]
.coerce
needs to ensure that this process eventually terminates, i.e., at least one of the two coerced return values should be one step closer to a builtin type.So, let's implement coerce
:
class A
def coerce(other) = [other, @value]
end
Now, if we run the code in the question, we get:
#<A:0x0000000101021cd0 @value=5>
15
Class preview: 5
Something method: 15
So, as you can see, the expression 10 + b.obj
was correctly evaluated to 15
.
The next problem with your code is that you are not overriding some of the standard methods that should always be overridden by objects. I am talking about methods like BasicObject#==
, Object#eql?
, Object#hash
, Object#to_s
, etc.
In particular, the message that is sent for displaying a human-readable debugging representation of an object, is inspect
. We have not overridden Object#inspect
, so we get the default implementation, which contains information about the class, an implementation-defined identifier, and a list of instance variables with their values.
We need to override inspect
to work more like this:
class A
def inspect = @value.inspect
end
Now, running the code in the question, produces the desired result:
5
15
Class preview: 5
Something method: 15
There are a couple of other things that could be improved in your code.
A
is intended to be number-like, it should inherit from Numeric
.A
is integer-like, it should respond to to_int
.to_int
, should logically also respond to to_i
.A
is number-like, it should implement the arithmetic operations.to_s
to provide a more sensible string representation.==
to provide more sensible equality semantics.eql?
and hash
to provide more sensible set membership and hash semantics.set_
, instead they should be named foo=
.Module#attr_reader
, Module#attr_writer
, or Module#attr_accessor
.If we put this all together, we get something like this:
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