В Ruby, как на самом деле работает coerce ()?


говорят, что когда у нас есть класс Point и умеет выполнять point * 3 следующим образом:

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

выход:

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

но потом

3 * point

не понял:

Point не может быть принужден к Fixnum (TypeError)

поэтому нам нужно дополнительно определить методом экземпляра coerce:

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

p 3 * point

выход:

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

так говорят, что 3 * point это то же самое, что 3.*(point). То есть метод экземпляра * принимает аргумент point и вызвать на объект 3.

теперь, так как этот метод * не знает, как умножить точку, так что

point.coerce(3)

будет вызван, и получить обратно массив:

[point, 3]

а то * еще раз применяется к нему, это правда?

теперь это понятно и у нас теперь есть новый Point объект, как выполняется метод экземпляра * на Point класса.

вопрос:

  1. кто вызывает point.coerce(3)? Это Рубин автоматически, или это какой-то код внутри * метод Fixnum ловить исключение? Или это по case утверждение, что когда он не знает один из известных типов, то вызовите coerce?

  2. тут coerce всегда нужно возвращать массив из 2 элементов? Может ли это быть не массив? Или это может быть массив из 3 элементов?

  3. и это правило, что исходный оператор (или метод) * будет вызван на элемент 0, с аргументом элемента 1? (Элемент 0 и элемент 1 - это два элемента в этом массиве, возвращаемые coerce.) Кто это делает? Это делается Ruby или это делается с помощью кода в Fixnum? Если это делается с помощью кода в Fixnum, то есть это" конвенция", которой все следуют, когда делают принуждение?

    так может это быть код в * на Fixnum делать что-то вроде этого:

    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. так что это действительно трудно что-то добавить к Fixnum ' s метод экземпляра coerce? В нем уже есть много кода, и мы не можем просто добавить несколько строк, чтобы улучшить его (но мы когда-нибудь захотим?)

  5. The coerce на Point класс довольно общий и он работает с * или + потому что они являются транзитивными. Что, если он не является транзитивным, например, если мы определите точку минус Fixnum, чтобы быть:

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

2 ответа:

короткий ответ: проверить как Matrix это.

идея в том, что coerce возвращает [equivalent_something, equivalent_self], где equivalent_something является объектом, в основном эквивалентным something но это знает, как делать операции на вашем Point класса. В Matrix lib, мы строим Matrix::Scalar любой Numeric объект, и этот класс знает, как выполнять операции над Matrix и Vector.

чтобы обратиться к вашему очки:

  1. Да, это Ruby напрямую (проверьте звонки на rb_num_coerce_bin источник), хотя ваши собственные типы должны делать тоже, если вы хотите, чтобы ваш код был расширяемым другими. Например, если ваш Point#* передается аргумент, который он не распознает, вы бы попросили этот аргумент до Point по телефону arg.coerce(self).

  2. Да, это должен быть массив из 2 элементов, таких, что b_equiv, a_equiv = a.coerce(b)

  3. да. Ruby делает это для встроенных типов, и Вы тоже должны использовать свои собственные типы, если хотите быть расширяемыми:

    def *(arg)
      if (arg is not recognized)
        self_equiv, arg_equiv = arg.coerce(self)
        self_equiv * arg_equiv
      end
    end
    
  4. идея в том, что вы не должны изменять Fixnum#*. Если он не знает, что делать, например, если аргумент Point, то он спросит вас, позвонив Point#coerce.

  5. транзитивность (или фактически коммутативность) не нужна, потому что оператор всегда звонил в правильном порядке. Это всего лишь призыв к coerce который временно возвращает полученный и аргумент. Нет встроенного механизма, который обеспечивает коммутативность операторов типа +,== и т. д...

если кто-то может придумать краткое, точное и понятное описание для улучшения официальной документации, оставьте комментарий!

Я часто пишу код по этому шаблону при работе с коммутативностью:

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