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:
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 yourPoint#*
is passed an argument it doesn't recognize, you would ask that argument tocoerce
itself to aPoint
by callingarg.coerce(self)
.Yes, it has to be an Array of 2 elements, such that
b_equiv, a_equiv = a.coerce(b)
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
endThe idea is that you shouldn't modify
Fixnum#*
. If it doesn't know what to do, for example because the argument is aPoint
, then it will ask you by callingPoint#coerce
.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
How to Switch to Ruby 1.9.3 Installed Using Homebrew
How to Set Up the Database.Yml File in Rails
String Interpolation in Ruby Doesn't Work
Ruby/Rails - Change the Timezone of a Time, Without Changing the Value
Parallel Assignment Operator in Ruby
Can't Access Rubygems - Possibly Due to Ssl
Mavericks, Rbenv, Your Ruby Version Is 2.0.0, But Your Gemfile Specified 2.1.1
Rails - Render :Action to Target Anchor Tag
Test If Variable Matches Any of Several Strings W/O Long If-Elsif Chain, or Case-When
Best Way to Add Comments in Erb
Why Not Use Shared Activerecord Connections For Rspec + Selenium
Long Running Delayed_Job Jobs Stay Locked After a Restart on Heroku
What Does "≪≪-" Mean in Ruby
Using God to Monitor Unicorn - Start Exited with Non-Zero Code = 1