Search code examples
rubyarrayscoerce

Coercing from Arrays


Suppose I have this simple class:

class Color
  attr_accessor :rgb
  def initialize(ary)
    @rgb = ary
  end
  def +(other)
    other = Color.new(other) unless Color === other
    Color.new(@rgb.zip(other.rgb).map {|p| [p.reduce(:+), 255].min })
  end
end

I know this is a bad way to implement it but this is the shortest way I can think.

c100 = Color.new([100, 100, 100])
c100 + c100         #=> Color(200, 200, 200)
c100 + c100 + c100  #=> Color(255, 255, 255)

It also works if I give an Array as Colors:

c100 + [50, 50, 50] #=> Color(150, 150, 150)

But I can't to this:

[50, 50, 50] + c100 #=> TypeError: can't convert Color into Array

Defining coerce doesn't work. How can I make it working?


Solution

  • It's because the code

    [50, 50, 50] + c100
    

    calls the + method on Array, not Color, and that method can't convert a color to an Array.

    By contrast,

     c100 + [50, 50, 50]
    

    does call Color's + method.

    However, even if you define a conversion method in Color:

    class Color
    def to_ary
    return @rgb
    end
    end
    

    the Array method will not work as you expect; the result will be the concatenation of the two arrays, since Array's + method concatenates their operands, rather than adding their elements:

    irb>[50,50,50]+c100
    => [50,50,50,100,100,100]
    

    Here, the result will be an Array, rather than a Color.

    EDIT:

    The only way I see of making this work is to alias the + method of Array to handle the special case of receiving a Color as the second operand. However, I will admit that this approach is rather ugly.

    class Array
      alias color_plus +
      def +(b)
        if b.is_a?(Color)
          return b+self
        end
        return color_plus(b)
      end
    end