In Ruby, How Does Coerce() Actually Work

In Ruby, how does coerce() actually work?

Short answer: check out how Matrix is doing it.

The idea is that coerce returns [equivalent_something, equivalent_self], where equivalent_something is an object basically equivalent to something but that knows how to do operations on your Point class. In the Matrix lib, we construct a Matrix::Scalar from any Numeric object, and that class knows how to perform operations on Matrix and Vector.

To address your points:

  1. Yes, it is Ruby directly (check calls to rb_num_coerce_bin in the source), although your own types should do too if you want your code to be extensible by others. For example if your Point#* is passed an argument it doesn't recognize, you would ask that argument to coerce itself to a Point by calling arg.coerce(self).

  2. Yes, it has to be an Array of 2 elements, such that b_equiv, a_equiv = a.coerce(b)

  3. Yes. Ruby does it for builtin types, and you should too on your own custom types if you want to be extensible:

    def *(arg)
    if (arg is not recognized)
    self_equiv, arg_equiv = arg.coerce(self)
    self_equiv * arg_equiv
    end
    end
  4. The idea is that you shouldn't modify Fixnum#*. If it doesn't know what to do, for example because the argument is a Point, then it will ask you by calling Point#coerce.

  5. Transitivity (or actually commutativity) is not necessary, because the operator is always called in the right order. It's only the call to coerce which temporarily reverts the received and the argument. There is no builtin mechanism that insures commutativity of operators like +, ==, etc...

If someone can come up with a terse, precise and clear description to improve the official documentation, leave a comment!

In Ruby, can the coerce() method know what operator it is that requires the help to coerce?

The point of coerce is not to know what operation you are trying to perform. Its purpose is to bring the argument and self to a common ground. Additionally, same operators can be commutative in certain classes, and not in other (Numeric#+ and Array#+, for example), so your small commutativity-based coerce exploit really won't pay off.

Instead of pushing your coerce to do what it's not intended to, you should create a new class instead (such as ScalarPoint, for example), and use it to interface scalar values with your Point:

class ScalarPoint
attr_reader :val

def initialize(val)
@val = val
end

def +(arg)
case arg
when Point:
Point.new(@val + arg.x, @val + arg.y)
when ScalarPoint:
ScalarPoint.new(arg.val + @val)
else
raise "Unknown operand!"
end
end

# define other operators, such as * etc

def coerce(arg)
return [ScalarPoint.new(arg), self] if arg.is_a?(Numeric)
raise "Can't handle"
end
end

and

class Point
def coerce(arg)
[ScalarPoint.new(arg), self] if arg.is_a?(Numeric)
raise "Can't handle"
end
end

etc. (NB: code not tested)

What's wrong with below ruby coerce method?

I think what you want is to add the << method to the Range class.

class Range
def <<(o)
self.to_a << o
end
end

Since you want to put << after an instance of a Range, you need to add such a method to the Range class. This will then return an array with the passed in item appended.

ruby coerce method not called as expected

coerce does coercions for numeric types. An Array is not a numeric type. Array#+ is not addition, it is concatenation, it behaves differently from numeric addition, e.g. [1, 2, 3] + [4, 5, 6] is not the same as [4, 5, 6] + [1, 2, 3].

ruby coercing integer and string

Let's break down your example into something a little more simple:

num1 = 1
num2 = 2
op = '+'
result = num1 + op + num2

When you attempt to assign result, here's what's actually happening:

result = 1 + '+' + 2

Ruby reports String can't be coerced into Integer because it is attempting to add the string + to the integer 1 and Ruby can't add a string to an integer. You can read more about this at String can't be coerced into Fixnum (TypeError).

You can reproduce it with an even simpler example:

1 + '+'
TypeError: String can't be coerced into Integer

In your question you say it should perform the calculation using those variables so I am assuming that you expect result to be equal to 3. (because it should calculate 1 + 2 from the three variables) That's not what happens in your result = ... line for the reasons described above.

The proper way to do this is to use public_send as described in these other questions and answers here on Stack Overflow:

  • Can I dynamically call a math operator in Ruby?
  • Ruby string to operator
  • Ruby- How to covert a string into an operator

Thus the answer is:

num1.public_send(op, num2)
=> 3

And integrated into your example:

if operators.include?(op)
result = num1.public_send(op, num2)
else
puts "enter a valid operator"
end

In Ruby, how to implement 20 - point and point - 20 using coerce()?

Subtraction is not a commutative operation, so you can't just swap operands in your coerce and expect it to work. coerce(something) should return [something_equivalent, self]. So, in your case I think you should write your Point#coerce like this:

def coerce(something)
if something.is_a?(Fixnum)
[Point.new(something, something), self]
else
[self, something]
end
end

You'd need to slightly change other methods, but I'll leave that to you.

Coerce to boolean

The obvious solution is to create a method in Kernel à la Array, Integer, String etc.:

module Kernel
def Boolean val
!!val
end
end

You could also add to_bool to Object, à la to_s:

class Object
def to_bool
!!self
end
end

Or do both and have one call the other. Barring either of these, I’d say !!x is the most common idiom, but is lesser known to those without a C background from what I’ve seen.

Really, this should probably be to_b to keep in-line with to_a vs. to_ary, etc. (read more). But to_b seems ambiguous to me (to bytes? to binary?).

Ruby: How ist the .+ method working internally?

Adding an integer to an object can be achieved by implementing +, e.g.:

class Foo
def initialize(value)
@value = value
end

def to_i
@value
end

def +(other)
Foo.new(to_i + other.to_i)
end
end

Foo.new(5) + 4
#=> #<Foo:0x00007fbd22050640 @value=9>

In order to add an instance of Foo to an integer, you have to also implement coerce which takes the left-hand value as an argument and returns an array with both values converted to Foo instances, e.g.:

class Foo
# ...

def coerce(other)
[Foo.new(other.to_i), self]
end
end

This gives you:

4 + Foo.new(5)
#=> #<Foo:0x00007fba600e3e28 @value=9>

The docs for Numeric contain another example.

Internally, coerce is called by Integer#+ if the argument is not an integer: (C code)

VALUE
rb_int_plus(VALUE x, VALUE y)
{
if (FIXNUM_P(x)) {
return fix_plus(x, y);
}
else if (RB_TYPE_P(x, T_BIGNUM)) {
return rb_big_plus(x, y);
}
return rb_num_coerce_bin(x, y, '+');
}

rb_num_coerce_bin calls coerce and then invokes the binary operator + on the returned values.

In Ruby this would be: (simplified)

class Integer
def +(other)
if other.is_a?(Integer)
# ...
else
x, y = other.coerce(self)
x + y
end
end
end

Coerce command('stdout') to integer for math comparison

Credit to @bodumin in the chef community slack #InSpec channel

Fuzzy typing with cmp matching

The simplest solution is to use the cmp matcher instead of the be matcher. While there are no explicit math operators given by example on [the official docs for cmp], math operations within a cmp match will evaluate properly

describe command('/tmp/foo') do 
its('stdout') { should cmp >= 0 }
its('stdout') { should cmp < 120 }
end

Per @aaronlippold

cmp is a logical compare that we do in the backend … aka 1 is true is TRUE is True. So in SSHD config as an example … you can set a prop using 1 or true or TRUE or True etc. Its a more forgiving matcher that we have built as much as a convince function etc.

How do I coerce a range of numbers separated by ':' into a list of the numbers in that range?

Thanks to Ben, who pointed out a similar problem:

unlist(lapply(unlist(strsplit(FAOCODES, ",")), function(x) eval(parse(text = x))))


Related Topics



Leave a reply



Submit