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 开发者_Python百科convert Color into Array
Defining coerce
doesn't work. How can I make it working?
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
Elaborating further on @peter-o answer I came up with this implementation that, although uggly in the sense that it redefines several methods of Array, it pretty much manages to be a good workaround for the expected behaviour, I don't think I would ever fit this in production code but I really liked the challenge... Sorry for diverging on the color subject but I didn't know what the expected behaviour for minus and times would be.
class Array
alias :former_plus :+
alias :former_minus :-
alias :former_times :*
def +(other)
former_plus(other)
rescue TypeError
apply_through_coercion(other, :+)
end
def -(other)
former_minus(other)
rescue TypeError
apply_through_coercion(other, :-)
end
def *(other)
former_times(other)
rescue TypeError
apply_through_coercion(other, :*)
end
# https://github.com/ruby/ruby/blob/ruby_1_9_3/lib/matrix.rb#L1385
def apply_through_coercion(obj, oper)
coercion = obj.coerce(self)
raise TypeError unless coercion.is_a?(Array) && coercion.length == 2
coercion[0].public_send(oper, coercion[1])
rescue
raise TypeError, "#{obj.inspect} can't be coerced into #{self.class}"
end
private :apply_through_coercion
end
One of the chalenges was to make sure the inverted call on the Point#-
method would not return unexpected results, hence the @coerced
instance variable as a control flag on the object.
class Point
attr_reader :x, :y
def initialize(x, y)
@x, @y, @coerced = x, y, false
end
def coerce(other)
@coerced = true
[self, other]
end
def coerced?; @coerced end
def +(other)
other = Point.new(*other) if other.respond_to? :to_ary
Point.new(@x + other.x, @y + other.y)
end
def -(other)
other = Point.new(*other) if other.respond_to? :to_ary
if coerced?
@coerced = false; other + (-self)
else self + (-other) end
end
def -@; Point.new(-@x, -@y) end
def *(other)
case other
when Fixnum then Point.new(@x*other, @y*other)
when Point then Point.new(@x*other.x, @y*other.y)
when Array then self * Point.new(*other)
end
end
end
After all, what this code manages to achieve is adding coerce functionality to the Array class where it didn't exist, explicitly to methods Array#+
, Array#-
and Array#*
.
精彩评论