2010-05-09 27 views
56

On dit que lorsque nous avons une classe Point et sait comment effectuer point * 3 comme ce qui suit:Dans Ruby, comment fonctionne réellement 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 

Sortie:

#<Point:0x336094 @x=1, @y=2> 
#<Point:0x335fa4 @x=3, @y=6> 

mais,

3 * point 

n'est pas entendu:

Point ne peut pas être contraintes Fixnum (TypeError)

Nous devons donc définir davantage une méthode d'instance coerce:

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

p 3 * point 

Sortie:

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

Il est donc dit que 3 * point est le même que 3.*(point). Autrement dit, la méthode d'instance * prend un argument point et invoque l'objet 3.

Maintenant, puisque cette méthode * ne sait pas comment multiplier un point, donc

point.coerce(3) 

sera appelé, et retourner un tableau:

[point, 3] 

puis * est une fois à nouveau appliqué à cela, est-ce vrai?

Maintenant, cela est compris et nous avons maintenant un nouvel objet Point, tel que réalisé par la méthode d'instance * de la classe Point.

La question est:

  1. Qui invoque point.coerce(3)? Est-ce Ruby automatiquement, ou est-ce un code à l'intérieur de * méthode de Fixnum en attrapant une exception? Ou est-ce par case déclaration que quand il ne connaît pas l'un des types connus, alors appelez coerce?

  2. Est-ce que coerce doit toujours retourner un tableau de 2 éléments? Peut-il être aucun tableau? Ou peut-il être un tableau de 3 éléments?

  3. Et est la règle que l'opérateur (ou la méthode) d'origine * sera alors invoqué sur l'élément 0, avec l'argument de l'élément 1? (L'élément 0 et l'élément 1 sont les deux éléments de ce tableau renvoyés par coerce.) Qui le fait? Est-ce que c'est fait par Ruby ou est-ce fait par code dans Fixnum? Si c'est fait par code en Fixnum, alors c'est une "convention" que tout le monde suit en faisant une coercition?

    Ainsi pourrait-il être le code * de Fixnum faire quelque chose comme ceci:

    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. Il est vraiment difficile d'ajouter quelque chose à la méthode de l'instance Fixnumcoerce? Il a déjà beaucoup de code et nous ne pouvons pas ajouter quelques lignes à l'améliorer (mais nous voulons jamais?)

  5. Le coerce dans la classe Point est tout à fait générique et il fonctionne avec * ou + parce qu'ils sont transitifs. Que faire s'il est pas transitive, comme si nous définissons point moins Fixnum être:

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

C'est une excellente question! Je suis tellement content que je l'ai trouvé parce que cela m'ennuyait et jusqu'à maintenant, je ne pensais pas que c'était résoluable! – sandstrom

+0

Une bonne question. Merci de l'avoir mis. Cela fera économiser beaucoup d'heures de confusion à l'ingénieur, j'en suis sûr. – VaidAbhishek

Répondre

39

Réponse courte: consultez how Matrix is doing it.

L'idée est que coerce retours [equivalent_something, equivalent_self], où equivalent_something est un objet essentiellement équivalent à something mais qui sait comment faire des opérations sur votre classe Point. Dans le Matrix lib, nous construisons un Matrix::Scalar à partir de n'importe quel objet Numeric, et cette classe sait comment effectuer des opérations sur Matrix et Vector.

Pour répondre à vos points:

  1. Oui, il est directement Ruby (appels à vérifier rb_num_coerce_bin in the source), bien que vos propres types devraient faire aussi si vous voulez que votre code soit extensible par d'autres. Par exemple, si votre Point#* est passé un argument qu'il ne reconnaît pas, vous demanderez cet argument à coerce lui-même à un Point en appelant arg.coerce(self).

  2. Oui, il doit être un tableau de 2 éléments, tels que b_equiv, a_equiv = a.coerce(b)

  3. Oui. Ruby on fait pour les types builtin, et vous devriez aussi sur vos propres types personnalisés si vous voulez être extensible:

    def *(arg) 
        if (arg is not recognized) 
        self_equiv, arg_equiv = arg.coerce(self) 
        self_equiv * arg_equiv 
        end 
    end 
    
  4. L'idée est que vous ne devriez pas modifier Fixnum#*. S'il ne sait pas quoi faire, par exemple parce que l'argument est Point, il vous le demandera en appelant le Point#coerce.

  5. La transitivité (ou réellement la commutativité) n'est pas nécessaire, car l'opérateur est toujours appelé dans le bon ordre. C'est seulement l'appel à coerce qui retourne temporairement le reçu et l'argument. Il n'y a pas de mécanisme qui assure builtin commutativité des opérateurs comme +, ==, etc ...

Si quelqu'un peut trouver une description laconique, précise et claire pour améliorer la documentation officielle, laisser un commentaire!

+0

hm, la transitivité ne fait-elle pas réellement la différence? Par exemple, voir http://stackoverflow.com/questions/2801241/in-ruby-how-to-implement-20-point-and-point-20-using-coerce –

+0

Non, la transitivité ne joue aucun rôle et Ruby ne suppose pas que 'a - b' est la même chose que' - (b - a) 'ou quelque chose comme ça, pas même' a + b == b + a'. Qu'est-ce qui vous fait croire que je me trompe? Avez-vous vérifié la source de l'IRM? Pourquoi ne pas essayer de suivre la direction indiquée? –

+0

Je pense que l'OP voulait dire "symétrique" plutôt que "transitif". Dans tous les cas, je veux savoir comment vous écrivez 'coerce' de sorte que des opérateurs non-symétriques comme' -' puissent être implémentés dans une seule direction tout en gardant les opérateurs symétriques travaillant dans les deux sens. En d'autres termes 'a + 3 == 3 + a' et' 3 + a - 3 == a' mais '3 - a' déclenche une erreur. –

2

Je me retrouve à écrire du code souvent le long de ce modèle lorsqu'ils traitent avec commutatif:

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