2010-05-09 9 views
56

Se dice que cuando tenemos una clase Point y sabe cómo realizar point * 3 como la siguiente:En Ruby, ¿cómo funciona realmente Coerce()?

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 luego,

3 * point 

no es entendido:

Point no puede ser obligado a Fixnum (TypeError)

Así que tenemos que definir con más detalle un método de instancia coerce:

class Point 
    def coerce(something) 
    [self, something] 
    end 
end 

p 3 * point 

Salida:

#<Point:0x3c45a88 @x=3, @y=6> 

lo tanto, es dijo que 3 * point es lo mismo que 3.*(point). Es decir, el método de instancia * toma un argumento point e invoca en el objeto 3.

Ahora, ya que este método * no sabe cómo multiplicar un punto, por lo

point.coerce(3) 

se llamará, y volver una matriz:

[point, 3] 

y luego * es una vez de nuevo aplicado a él, ¿es cierto?

Ahora, esto se entiende y ahora tenemos un nuevo Point objeto, como se realizó por el método de instancia * de la clase Point.

La pregunta es:

  1. que invoca point.coerce(3)? ¿Es Ruby automáticamente, o es algún código dentro del método * de Fixnum al detectar una excepción? ¿O es por la declaración case que cuando no conoce uno de los tipos conocidos, llama al coerce?

  2. ¿El coerce siempre debe devolver una matriz de 2 elementos? ¿No puede ser una matriz? ¿O puede ser una matriz de 3 elementos?

  3. ¿Y es la regla que, el operador original (o método) * se invocará entonces en el elemento 0, con el argumento del elemento 1? (Elemento 0 y elemento 1 son los dos elementos en esa matriz devueltos por coerce.) ¿Quién lo hace? ¿Lo hace Ruby o está hecho por código en Fixnum? Si se hace por código en Fixnum, entonces ¿es una "convención" que todos siguen al hacer una coacción?

    Así podría ser el código en * de Fixnum hacer 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 
    
  4. lo tanto, es muy difícil añadir algo al método Fixnum 's ejemplo coerce? Ya cuenta con una gran cantidad de código en él y no podemos simplemente añada unas cuantas líneas para mejorar (pero nunca vamos a querer?)

  5. El coerce en la clase Point es bastante genérico y funciona con * o + porque son transitivos. ¿Y si no es transitivo, como si definimos punto negativo Fixnum sean:

    point = Point.new(100,100) 
    point - 20 #=> (80,80) 
    20 - point #=> (-80,-80) 
    
+1

Esta es una excelente pregunta! Estoy tan feliz de haberlo encontrado porque esto me ha estado molestando y hasta ahora no creía que pudiera resolverse. – sandstrom

+0

Una gran pregunta. Gracias por decirlo. Ahorrará muchas horas de confusión de ingenieros, estoy seguro. – VaidAbhishek

Respuesta

39

Respuesta corta: consulta how Matrix is doing it.

La idea es que coerce vuelve [equivalent_something, equivalent_self], donde equivalent_something es un objeto básicamente equivalente a something pero que sabe hacer operaciones en su clase Point. En el Matrix lib, construimos un Matrix::Scalar desde cualquier objeto Numeric, y esa clase sabe cómo realizar operaciones en Matrix y Vector.

Para hacer frente a sus puntos:

  1. sí, es directamente Rubí (comprobar las llamadas a rb_num_coerce_bin in the source), a pesar de sus propios tipos deben hacer también si usted quiere que su código sea extensible por otros. Por ejemplo, si su Point#* se pasa un argumento que no reconoce, le preguntaría ese argumento a coerce a un Point llamando al arg.coerce(self).

  2. Sí, tiene que ser una matriz de 2 elementos, de manera que b_equiv, a_equiv = a.coerce(b)

  3. Sí. Rubí lo hace por orden interna tipos, y se debe también en sus propios tipos personalizados si quieren ser extensible:

    def *(arg) 
        if (arg is not recognized) 
        self_equiv, arg_equiv = arg.coerce(self) 
        self_equiv * arg_equiv 
        end 
    end 
    
  4. La idea es que no se debe modificar Fixnum#*. Si no sabe qué hacer, por ejemplo porque el argumento es Point, le preguntará llamando al Point#coerce.

  5. La transitividad (o realmente la conmutatividad) no es necesaria, ya que el operador siempre se llama en el orden correcto. Es solo la llamada al coerce que revierte temporalmente el recibido y el argumento. No existe un mecanismo incorporado que asegura conmutatividad de los operadores como +, ==, etc ...

Si alguien puede llegar a una descripción concisa, precisa y clara para mejorar la documentación oficial, dejar un comentario!

+0

hm, ¿la transitividad realmente no hace la diferencia? Por ejemplo, consulte http://stackoverflow.com/questions/2801241/in-ruby-how-to-implement-20-point-and-point-20-using-coerce –

+0

No, la transitividad no juega ningún papel y Ruby no supone que 'a - b' sea lo mismo que' - (b - a) 'o algo por el estilo, ni siquiera' a + b == b + a'. ¿Qué te hace creer que estoy equivocado? ¿Revisaste la fuente de MRI? ¿Por qué no intentar seguir la dirección que indico? –

+0

Creo que el OP significaba "simétrico" en lugar de "transitivo". En cualquier caso, ** I ** quiero saber cómo se escribe 'coerción ', de modo que los operadores no simétricos como' -' puedan implementarse en una sola dirección, manteniendo a los operadores simétricos funcionando en ambos sentidos. En otras palabras, 'a + 3 == 3 + a' y' 3 + a - 3 == a' pero '3 - a' genera un error. –

2

me encuentro a menudo la escritura de 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 
Cuestiones relacionadas