En Ruby, ¿cómo funciona coerce()?
coercion type-coercion (2)
Se dice que cuando tenemos un Point
clase y sabemos cómo realizar el point * 3
la siguiente manera:
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
Salida:
#<Point:0x336094 @x=1, @y=2>
#<Point:0x335fa4 @x=3, @y=6>
pero entonces,
3 * point
no se entiende:
Point
no puede ser coaccionado enFixnum
(TypeError
)
Entonces, necesitamos definir aún más una coerce
método de instancia:
class Point
def coerce(something)
[self, something]
end
end
p 3 * point
Salida:
#<Point:0x3c45a88 @x=3, @y=6>
Entonces se dice que el 3 * point
es lo mismo que 3.*(point)
. Es decir, el método de instancia *
toma un point
argumento e invoca en el objeto 3
.
Ahora, dado que este método *
no sabe cómo multiplicar un punto, entonces
point.coerce(3)
se llamará y obtendrá una matriz:
[point, 3]
y luego *
se aplica una vez más a él, ¿es eso cierto?
Ahora, esto se comprende y ahora tenemos un nuevo objeto Point
, como lo realiza el método de instancia *
de la clase Point
.
La pregunta es:
¿Quién invoca a
point.coerce(3)
? ¿Es Ruby automáticamente, o es algún código dentro del*
método deFixnum
mediante la captura de una excepción? ¿O es por declaración decase
que cuando no conoce uno de los tipos conocidos, llamacoerce
?¿La
coerce
siempre necesita devolver una matriz de 2 elementos? ¿No puede ser una matriz? ¿O puede ser una matriz de 3 elementos?¿Y es la regla que, el operador (o método)
*
se invocará en el elemento 0, con el argumento del elemento 1? (El elemento 0 y el elemento 1 son los dos elementos en esa matriz devueltos porcoerce
.) ¿Quién lo hace? ¿Lo hace Ruby o está hecho por código enFixnum
? Si se hace por código enFixnum
, entonces ¿es una "convención" que todos siguen al hacer una coacción?Entonces, ¿podría ser el código en
*
deFixnum
haciendo algo como esto?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
Entonces, ¿es realmente difícil agregar algo a la
Fixnum
del método de instancia deFixnum
? Ya tiene un montón de código y no podemos simplemente agregar algunas líneas para mejorarlo (¿pero alguna vez querremos?)La
coerce
en la clasePoint
es bastante genérica y funciona con*
o+
porque son transitivos. ¿Qué pasa si no es transitivo, como si definiéramos que Point menos Fixnum es:point = Point.new(100,100) point - 20 #=> (80,80) 20 - point #=> (-80,-80)
Respuesta corta: echa un vistazo a cómo lo está haciendo Matrix
.
La idea es que coerce
devuelve [equivalent_something, equivalent_self]
, donde equivalent_something
es un objeto básicamente equivalente a something
pero que sabe cómo hacer operaciones en su clase Point
. En Matrix
lib, construimos un Matrix::Scalar
partir de cualquier objeto Numeric
, y esa clase sabe cómo realizar operaciones en Matrix
y Vector
.
Para abordar sus puntos:
Sí, es Ruby directamente (verifique las llamadas a
rb_num_coerce_bin
en la fuente ), aunque sus propios tipos también deberían hacerlo si desea que su código sea extensible por otros. Por ejemplo, si suPoint#*
es pasado un argumento que no reconoce, usted le pediría a ese argumento que secoerce
a unPoint
llamando aarg.coerce(self)
.Sí, tiene que ser una matriz de 2 elementos, tal que
b_equiv, a_equiv = a.coerce(b)
Sí. Ruby lo hace para los tipos integrados, y también debería hacerlo en sus propios tipos personalizados si desea ser extensible:
def *(arg) if (arg is not recognized) self_equiv, arg_equiv = arg.coerce(self) self_equiv * arg_equiv end end
La idea es que no se debe modificar
Fixnum#*
. Si no sabe qué hacer, por ejemplo, porque el argumento es unPoint
, le preguntará llamando aPoint#coerce
.La transitividad (o realmente la conmutatividad) no es necesaria, porque el operador siempre se llama en el orden correcto. Es solo el llamado a
coerce
que revierte temporalmente el recibido y el argumento. No hay un mecanismo incorporado que asegure la conmutatividad de operadores como+
,==
, etc ...
Si alguien puede presentar una descripción concisa, precisa y clara para mejorar la documentación oficial, ¡deje un comentario!
A menudo me encuentro escribiendo código a lo largo de este patrón cuando se trata de conmutatividad:
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