Dans Ruby, comment coerce () fonctionne-t-il réellement?

On dit que lorsque nous avons une classe Point et que nous soaps comment exécuter le point * 3 comme suit:

 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:

 # # 

mais alors,

 3 * point 

n’est pas compris:

Point ne peut pas être forcé dans Fixnum ( TypeError )

Nous devons donc définir plus précisément une méthode d’instance coerce :

 class Point def coerce(something) [self, something] end end p 3 * point 

Sortie:

 # 

On dit donc que le 3 * point est le même que le point 3.*(point) . C’est-à-dire que la méthode d’instance * prend un point argument et invoque l’object 3 .

Maintenant, comme cette méthode * ne sait pas comment multiplier un point, alors

 point.coerce(3) 

sera appelé, et récupérer un tableau:

 [point, 3] 

et puis * est encore une fois appliqué, est-ce vrai?

Maintenant, cela est compris et nous avons maintenant un nouvel object Point , tel qu’exécuté 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 dans la méthode * de Fixnum en capturant une exception? Ou est-ce par déclaration de case que quand il ne connaît pas l’un des types connus, alors appelez coerce ?

  2. La coerce doit-elle toujours renvoyer un tableau de 2 éléments? Est-ce que ce n’est pas un tableau? Ou peut-il être un tableau de 3 éléments?

  3. Et la règle est-elle que l’opérateur (ou la méthode) original * sera alors invoqué sur l’élément 0, avec l’argument de l’élément 1? (Element 0 et element 1 sont les deux éléments de ce tableau renvoyés par coerce .) Qui le fait? Est-ce fait par Ruby ou est-il fait par code dans Fixnum ? Si cela se fait par code dans Fixnum , alors c’est une “convention” que tout le monde suit quand on fait une coercition?

    Ainsi pourrait-il être le code dans * de Fixnum faisant 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 donc difficile d’append quelque chose à la méthode de coerce de Fixnum . Il contient déjà beaucoup de code et nous ne pouvons pas simplement append quelques lignes pour l’améliorer (mais le voudrons-nous jamais?)

  5. La coerce dans la classe Point est assez générique et fonctionne avec * ou + car ils sont transitifs. Et si ce n’est pas transitif, par exemple si on définit Point moins Fixnum comme étant:

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

Réponse courte: Découvrez comment Masortingx fait .

L’idée est que la coerce retourne [equivalent_something, equivalent_self] , où equivalent_something est un object équivalent à something mais qui sait faire des opérations sur votre classe Point . Dans la librairie Masortingx , nous construisons une Masortingx::Scalar partir de n’importe quel object Numeric , et cette classe sait comment effectuer des opérations sur la Masortingx et le Vector .

Pour répondre à vos points:

  1. Oui, c’est Ruby directement (vérifiez les appels à rb_num_coerce_bin dans la source ), même si vos propres types devraient faire de même si vous voulez que votre code soit extensible par d’autres. Par exemple, si votre argument Point#* est passé à un argument qu’il ne reconnaît pas, vous devez demander à cet argument de se coerce à un Point en appelant arg.coerce(self) .

  2. Oui, il doit s’agir d’un tableau de 2 éléments, tel que b_equiv, a_equiv = a.coerce(b)

  3. Oui. Ruby le fait pour les types intégrés, et vous devriez également utiliser 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 un Point , il vous le demandera en appelant Point#coerce .

  5. La transitivité (ou la commutativité réelle) n’est pas nécessaire, car l’opérateur est toujours appelé dans le bon ordre. Ce n’est que l’appel à la coerce qui coerce temporairement le reçu et l’argument. Il n’y a pas de mécanisme intégré qui assure la commutativité des opérateurs comme + , == , etc …

Si quelqu’un peut proposer une description concise, précise et claire pour améliorer la documentation officielle, laissez un commentaire!

Je me trouve souvent à écrire du code sur ce modèle lorsque je traite de la commutativité:

 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