It is said that when we have a class Point
and knows how to perform point * 3
like the following:
class Point
def initialize(x,y)
@x, @y = x, y
end
def *(c)
Point.new(@x * c, @y * c)
end
end
point = Point.new(1,2)
p point
p point * 3
Output:
#<Point:0x336094 @x=1, @y=2>
#<Point:0x335fa4 @x=3, @y=6>
but then,
3 * point
is not understood:
Point
can't be coerced intoFixnum
(TypeError
)
So we need to further define an instance method coerce
:
class Point
def coerce(something)
[self, something]
end
end
p 3 * point
Output:
#<Point:0x3c45a88 @x=3, @y=6>
So it is said that 3 * point
is the same as 3.*(point)
. That is, the instance method *
takes an argument point
and invoke on the object 3
.
Now, since this method *
doesn't know how to multiply a point, so
point.coerce(3)
will be called, and get back an array:
[point, 3]
and then *
is once again applied to it, is that true?
Now, this is understood and we now have a new Point
object, as performed by the instance method *
of the Point
class.
The question is:
Who invokes point.coerce(3)
? Is it Ruby automatically, or is it some code inside of *
method of Fixnum
by catching an exception? Or is it by case
statement that when it doesn't know one of the known types, then call coerce
?
Does coerce
always need to return an array of 2 elements? Can it be no array? Or can it be an array of 3 elements?
And is the rule that, the original operator (or method) *
will then be invoked on element 0, with the argument of element 1? (Element 0 and element 1 are the two elements in that array returned by coerce
.) Who does it? Is it done by Ruby or is it done by code in Fixnum
? If it is done by code in Fixnum
, then it is a "convention" that everybody follows when doing a coercion?
So could it be the code in *
of Fixnum
doing something like this:
class Fixnum
def *(something)
if (something.is_a? ...)
else if ... # other type / class
else if ... # other type / class
else
# it is not a type / class I know
array = something.coerce(self)
return array[0].*(array[1]) # or just return array[0] * array[1]
end
end
end
So it is really hard to add something to Fixnum
's instance method coerce
? It already has a lot of code in it and we can't just add a few lines to enhance it (but will we ever want to?)
The coerce
in the Point
class is quite generic and it works with *
or +
because they are transitive. What if it is not transitive, such as if we define Point minus Fixnum to be:
point = Point.new(100,100)
point - 20 #=> (80,80)
20 - point #=> (-80,-80)
I find myself often writing code along this pattern when dealing with commutativity:
class Foo
def initiate(some_state)
#...
end
def /(n)
# code that handles Foo/n
end
def *(n)
# code that handles Foo * n
end
def coerce(n)
[ReverseFoo.new(some_state),n]
end
end
class ReverseFoo < Foo
def /(n)
# code that handles n/Foo
end
# * commutes, and can be inherited from Foo
end
Short answer: check out how Matrix
is doing it.
The idea is that coerce
returns [equivalent_something, equivalent_self]
, where equivalent_something
is an object basically equivalent to something
but that knows how to do operations on your Point
class. In the Matrix
lib, we construct a Matrix::Scalar
from any Numeric
object, and that class knows how to perform operations on Matrix
and Vector
.
To address your points:
Yes, it is Ruby directly (check calls to rb_num_coerce_bin
in the source), although your own types should do too if you want your code to be extensible by others. For example if your Point#*
is passed an argument it doesn't recognize, you would ask that argument to coerce
itself to a Point
by calling arg.coerce(self)
.
Yes, it has to be an Array of 2 elements, such that b_equiv, a_equiv = a.coerce(b)
Yes. Ruby does it for builtin types, and you should too on your own custom types if you want to be extensible:
def *(arg)
if (arg is not recognized)
self_equiv, arg_equiv = arg.coerce(self)
self_equiv * arg_equiv
end
end
The idea is that you shouldn't modify Fixnum#*
. If it doesn't know what to do, for example because the argument is a Point
, then it will ask you by calling Point#coerce
.
Transitivity (or actually commutativity) is not necessary, because the operator is always called in the right order. It's only the call to coerce
which temporarily reverts the received and the argument. There is no builtin mechanism that insures commutativity of operators like +
, ==
, etc...
If someone can come up with a terse, precise and clear description to improve the official documentation, leave a comment!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With