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é dansFixnum
(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:
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
?
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?
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
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?)
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:
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)
.
Oui, il doit s’agir d’un tableau de 2 éléments, tel que b_equiv, a_equiv = a.coerce(b)
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
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
.
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