Подтвердить что ты не робот

В 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.

Возникает вопрос:

  • Кто вызывает point.coerce(3)? Является ли это Ruby автоматически, или это какой-то код внутри метода * Fixnum, вылавливая исключение? Или это оператор case, что, когда он не знает один из известных типов, вызовите coerce?

  • Требуется ли coerce вернуть массив из двух элементов? Разве это не массив? Или это может быть массив из трех элементов?

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

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

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

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

Ответ 1

Короткий ответ: как Matrix делает это.

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

Чтобы указать свои баллы:

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

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

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

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

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

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

Ответ 2

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

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