В Ruby, как работает функция coerce()?
Говорят, что когда мы имеем класс Point
и знаем, как выполнить point * 3
следующим образом:
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
Вывод:
#<Point:0x336094 @x=1, @y=2>
#<Point:0x335fa4 @x=3, @y=6>
но затем
3 * point
не понимается:
Point
не может быть принудительно введен в Fixnum
(TypeError
)
Итак, нам нужно дополнительно определить метод экземпляра coerce
:
class Point
def coerce(something)
[self, something]
end
end
p 3 * point
Вывод:
#<Point:0x3c45a88 @x=3, @y=6>
Итак, говорят, что 3 * point
совпадает с 3.*(point)
. То есть метод экземпляра *
принимает аргумент Point
и вызывает объект 3
.
Теперь, поскольку этот метод *
не знает, как умножить точку, поэтому
point.coerce(3)
будет вызываться и вернуть массив:
[point, 3]
а затем *
снова применяется к нему, это правда?
Теперь это понимается, и теперь у нас есть новый объект Point
, который выполняется методом экземпляра *
класса Point
.
Возникает вопрос:
-
Кто вызывает point.coerce(3)
? Является ли это Ruby автоматически, или это какой-то код внутри метода *
Fixnum
, вылавливая исключение? Или это оператор case
, что, когда он не знает один из известных типов, вызовите coerce
?
-
Требуется ли coerce
вернуть массив из двух элементов? Разве это не массив? Или это может быть массив из трех элементов?
-
И является ли правило, что исходный оператор (или метод) *
будет вызываться в элементе 0 с аргументом элемента 1? (Элемент 0 и элемент 1 - это два элемента в этом массиве, возвращаемые coerce
.) Кто это? Это делается Ruby или выполняется с помощью кода в Fixnum
? Если это делается с помощью кода в Fixnum
, то это "соглашение", которое каждый следует при принуждении?
Так может ли быть код в *
of Fixnum
делать что-то вроде этого:
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
-
Так что действительно сложно добавить что-то к Fixnum
методу экземпляра coerce
? В нем уже много кода, и мы не можем просто добавить несколько строк для его улучшения (но будем ли мы когда-либо хотеть?)
-
coerce
в классе Point
является довольно общим и работает с *
или +
, потому что они являются транзитивными. Что, если это не транзитивно, например, если мы определяем Point минус Fixnum следующим образом:
point = Point.new(100,100)
point - 20 #=> (80,80)
20 - point #=> (-80,-80)
Ответы
Ответ 1
Короткий ответ: как Matrix
делает это.
Идея состоит в том, что coerce
возвращает [equivalent_something, equivalent_self]
, где equivalent_something
- это объект, в основном эквивалентный something
, но он знает, как выполнять операции над вашим классом Point
. В Matrix
lib мы создаем Matrix::Scalar
из любого объекта Numeric
, и этот класс знает, как выполнять операции над Matrix
и Vector
.
Чтобы указать свои баллы:
-
Да, это Ruby напрямую (проверьте на rb_num_coerce_bin
в источнике), хотя ваши собственные типы тоже должны делать если вы хотите, чтобы ваш код расширялся другими. Например, если ваш Point#*
передан аргумент, который он не распознает, вы должны задать этот аргумент coerce
себе Point
, вызвав arg.coerce(self)
.
-
Да, это должен быть массив из 2 элементов, такой как b_equiv, a_equiv = a.coerce(b)
-
Да. Ruby делает это для встроенных типов, и вы также должны использовать свои собственные типы, если хотите быть расширяемыми:
def *(arg)
if (arg is not recognized)
self_equiv, arg_equiv = arg.coerce(self)
self_equiv * arg_equiv
end
end
-
Идея состоит в том, что вы не должны изменять Fixnum#*
. Если он не знает, что делать, например, потому что аргумент Point
, то он попросит вас, вызывая Point#coerce
.
-
Транзитивность (или фактически коммутативность) не требуется, так как оператор всегда вызывается в правильном порядке. Это только вызов coerce
, который временно возвращает полученный и аргумент. Не существует встроенного механизма, обеспечивающего коммутативность операторов типа +
, ==
и т.д.
Если кто-то может придумать краткое, точное и четкое описание для улучшения официальной документации, оставьте комментарий!
Ответ 2
Я часто пишу код по этому шаблону при работе с коммутативностью:
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