在Ruby中,coerce()方法实际上是如何工作的?

62
据说我们有一个类,并且知道如何执行,就像以下代码所示:

它被认为能够执行 point * 3 。

<code>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
</code>

输出:

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

但是,

3 * point

不被理解:

Point无法强制转换为FixnumTypeError

因此,我们需要进一步定义一个实例方法coerce

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

p 3 * point

输出:

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

据说3 * point3.*(point)相同。也就是说,实例方法*接受一个参数point并在对象3上调用。

现在,由于这个方法*不知道如何将一个点乘以3,所以

point.coerce(3)

将被调用,并返回一个数组:

[point, 3]

然后再次对其应用*,这是真的吗?

现在,我们已经理解了这一点,我们现在有了一个新的Point对象,就像Point类的实例方法*所执行的那样。

问题是:

  1. 谁调用了point.coerce(3)?是Ruby自动调用的,还是Fixnum*方法内部通过捕获异常来调用的?或者当它不知道一个已知类型时,是否通过case语句来调用coerce

  2. coerce总是需要返回一个由两个元素组成的数组吗?它可以没有数组吗?或者可以是由三个元素组成的数组吗?

  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的实例方法coerce添加代码真的很难吗?它已经有了许多代码,我们不能仅添加几行来增强它(但我们是否真的需要这样做呢?)

  5. Point类中的coerce非常通用,它可以使用*+运算符,因为它们是可传递的。如果不可传递,比如我们定义Point减去Fixnum的操作:

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

1
这是一个非常好的问题!我很开心找到它,因为这一直困扰着我,直到现在我才认为它是可以解决的! - sandstrom
一个非常好的问题,感谢您提出它。我相信它将会节省很多工程师困惑的时间。 - VaidAbhishek
2个回答

45
短回答:请查看Matrix是如何做到这一点的
其中的想法是coerce返回[equivalent_something, equivalent_self],其中equivalent_something是一个对象,基本上相当于something,但知道如何在您的Point类上执行操作。在Matrix库中,我们从任何Numeric对象构造一个Matrix::Scalar,并且该类知道如何对MatrixVector执行操作。
为了解决您的问题:
  1. 是的,它直接使用Ruby(请检查源代码中对rb_num_coerce_bin的调用),尽管如果您希望您的代码可扩展性更强,您自己的类型也应该这样做。例如,如果您的Point#*传递了一个它不识别的参数,则会要求该参数通过调用arg.coerce(self)来将其转换为Point

  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的调用才会暂时恢复接收和参数。没有内置机制可以确保像+==等运算符的交换律。

如果有人能够提供简洁、准确、清晰的描述来改进官方文档,请留下评论!


嗯,传递性确实会有影响吗?例如,参见 http://stackoverflow.com/questions/2801241/in-ruby-how-to-implement-20-point-and-point-20-using-coerce - nonopolarity
1
不,传递性没有任何作用,Ruby 不会假设 a - b-(b - a) 或任何类似的内容相同,甚至不是 a + b == b + a。是什么让您相信我错了?您有检查MRI源代码吗?为什么不试着按照我指示的方向尝试一下呢? - Marc-André Lafortune
我认为OP的意思是“对称”而不是“传递性”。无论如何,我想知道您如何编写coerce函数,使得非对称运算符(如-)只能单向实现,同时保持对称运算符双向工作。换句话说,a + 3 == 3 + a3 + a - 3 == a,但3 - a会引发错误。 - Old Pro
@OldPro,通过“对称”,我认为您的意思是“交换律”。但我同意,OP可能指的是交换律,而不是传递性。传递性与关系(例如相等)有关,而不是像加法/减法这样的操作。 - Kelvin
没有什么能保证可交换性。即使没有涉及强制转换。例如,请参见http://blog.marc-andre.ca/2009/05/02/schizo-ruby-puzzle/。 - Marc-André Lafortune

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

1
对于未来阅读此内容的任何人:如果可以直接将数字转换为您的格式,则建议使用 [Foo.instantiate(n), self] - daboross

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接