60 votes

En Ruby, comment coerce() fonctionnent réellement ?

Il est dit que lorsque nous avons une classe Point et sait comment effectuer 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:

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

mais alors,

3 * point

n'est pas compris:

Point ne peut pas être contraint Fixnum (TypeError)

Donc, nous avons besoin de définir 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>

Donc, il est dit qu' 3 * point est le même que 3.*(point). Qui est, la méthode d'instance * prend un argument point et invoquer sur l'objet 3.

Maintenant, étant donné que cette méthode * ne sait pas comment multiplier un point, de sorte

point.coerce(3)

sera appelé, et obtenir un tableau:

[point, 3]

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

Maintenant, cela est compris et nous avons maintenant un nouveau Point objet, telle qu'effectuée par la méthode d'instance * de la Point classe.

La question est:

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

  2. N' coerce toujours besoin de retourner un tableau de 2 éléments? Peut-elle pas être ensemble? Ou peut-il être un tableau de 3 éléments?

  3. Et la règle est que, l'original de l'opérateur (ou une méthode) * sera alors appelée 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 dans ce tableau retourné par coerce.) Qui est-elle? Est-il fait en Ruby ou est-il fait par code en Fixnum? Si c'est fait par code en Fixnum, alors que c'est une "convention" que tout le monde suit lorsque vous faites une contrainte?

    Ainsi pourrait-il être le code en * de Fixnum de 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. Donc, il est vraiment difficile d'ajouter quelque chose à l' Fixnums'méthode d'instance coerce? Il a déjà beaucoup de code et on ne peut pas simplement ajouter quelques lignes à l'améliorer (mais pourrons-nous jamais?)

  5. L' coerce dans la Point classe est assez générique, et il fonctionne avec * ou + parce qu'ils sont transitifs. Si ce n'est pas transitive, comme si l'on définit le Point moins Fixnum:

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

42voto

Marc-André Lafortune Points 34140

Réponse courte: découvrez comment Matrix est de le faire.

L'idée est qu' coerce retours [equivalent_something, equivalent_self]equivalent_something est un objet essentiellement équivalent à something mais qui sait comment faire les opérations sur l' Point classe. Dans l' Matrix lib, nous construisons un Matrix::Scalar partir Numeric objet, et que la classe sait comment effectuer des opérations sur Matrix et Vector.

Pour répondre à vos points:

  1. Oui, c'est Ruby directement (cochez les appels d' rb_num_coerce_bin dans la source), bien que vos propres types devrait faire trop si vous voulez que votre code pour être extensible par d'autres. Par exemple, si votre Point#* est passé à un argument qu'il ne reconnaît pas, vous pouvez demander que l'argument coerce d'une Point en appelant arg.coerce(self).

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

  3. Oui. Ruby-t-il pour builtin types, et vous devriez aussi sur votre propre si vous souhaitez ê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 devez pas modifier Fixnum#*. Si il ne sait pas quoi faire, par exemple parce que l'argument est un Point, ensuite il va vous demander en appelant Point#coerce.

  5. Transitivité (ou en fait 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 revient temporairement le reçu et l'argument. Il n'y a pas de builtin mécanisme qui assure la commutativité des opérateurs comme +, ==, etc...

Si quelqu'un peut venir avec un laconique description claire et précise afin d'améliorer la documentation officielle, laissez un commentaire!

2voto

stobix Points 36

Je me trouve souvent en train d'écrire du code suivant ce modèle quand on parle de 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
 

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X