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:
Pointcan’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 ofFixnumby catching an exception? Or is it bycasestatement that when it doesn’t know one of the known types, then callcoerce? -
Does
coercealways 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 bycoerce.) Who does it? Is it done by Ruby or is it done by code inFixnum? If it is done by code inFixnum, then it is a “convention” that everybody follows when doing a coercion?So could it be the code in
*ofFixnumdoing 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 methodcoerce? 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
coercein thePointclass 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)
Short answer: check out how
Matrixis doing it.The idea is that
coercereturns[equivalent_something, equivalent_self], whereequivalent_somethingis an object basically equivalent tosomethingbut that knows how to do operations on yourPointclass. In theMatrixlib, we construct aMatrix::Scalarfrom anyNumericobject, and that class knows how to perform operations onMatrixandVector.To address your points:
Yes, it is Ruby directly (check calls to
rb_num_coerce_binin the source), although your own types should do too if you want your code to be extensible by others. For example if yourPoint#*is passed an argument it doesn’t recognize, you would ask that argument tocoerceitself to aPointby callingarg.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:
The idea is that you shouldn’t modify
Fixnum#*. If it doesn’t know what to do, for example because the argument is aPoint, then it will ask you by callingPoint#coerce.Transitivity (or actually commutativity) is not necessary, because the operator is always called in the right order. It’s only the call to
coercewhich 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!