Reputation: 7308
In Ruby, if you try to do 2 + nil
, for example, you get
TypeError: nil can't be coerced into Integer
but, this made me wonder if there is anything that can be coerced into an Integer
. A lot of times Integer
s are coerced to other numeric types, like here
(2 + Rational('1/2')).class
# => Rational
(2 + 0.5).class
# => Float
(2 + 1i).class
# => Complex
And, Booleans in Ruby don't get coerced to Integer
. So, what does get coerced into an Integer
?
Upvotes: 5
Views: 5647
Reputation: 369498
There are four protocols for types coercion/conversion in Ruby, three general ones, and one specific to arithmetic operations on numbers.
When objects can meaningfully be represented as an instance of another class, then they respond to one-letter type conversion methods such as to_s
, to_i
, to_f
, etc. The bar for responding to one of those is very low: if there is any way that an object can be represented as, say, an integer that is not completely bogus, then it can respond to to_i
. E.g. it kinda-sorta makes sense to represent nil
as the integer 0
, therefore nil
responds to to_i
. Likewise, it kinda-sorta makes sense to represent nil
as the empty string, therefore it responds to to_s
.
This is much stricter. Only when an object can be reasonably be interpreted as being of the same type as an instance of a different class, is it allowed to respond to to_str
, to_int
, etc. For example, there are 10 implementations of to_i
in the Ruby core library, but only 4 for to_int
.
In pure OO, it shouldn't matter what class an object is an instance of. If an object behaves like an integer, then it is, for all intents and purposes, an integer, no matter what class it is an instance of. Unfortunately, Ruby is not purely OO. For example, on YARV, Array#[]
is implemented by indexing into a C array using a machine integer. (On JRuby, it is indexing into a Java array, and similarly on Opal, MRuby, Rubinius, IronRuby, MagLev, …) This means that only objects that are actual instances of the Integer
class can be used for array indexing, because the Ruby implementation only knows how to extract a machine integer (Java integer, …) from an Integer
, but not from an arbitrary object that behaves like an integer; it knows what the internal memory structure of an Integer
looks like, but it doesn't know what the internal memory structure of your MyInteger
looks like.
But, as an escape hatch, Array#[]
will try to convert the object to an integer using to_int
first. So, it's not fully OO, but at least at gives you the option to participate in array indexing by converting yourself to an integer.
Likewise, the &
unary ampersand prefix "convert-to-block" operator theoretically only works with Proc
s, but it will call to_proc
first, to give the object a chance to convert itself to a Proc
.
There are a couple of factory methods defined in Kernel
whose name matches a core class, e.g. Kernel#Integer
, Kernel#Float
, etc. Those are intended to convert a wide range of objects to their respective classes. They are in some sense much more loose, in that they accept a much wider range of objects than the multi-letter type conversions. But in some sense they are also stricter, since they are allowed to raise exceptions. The to_
methods aren't allowed to raise exceptions: they either exist, and then they must work, or they don't exist at all.
Kernel#Integer
accepts a string, for example, and will try to parse it as an integer.
This protocol is specific to numbers, and to arithmetic operations. When you send a number a message for an arithmetic operation, and pass an argument that the number doesn't know how to deal with, it will ask that argument to coerce
the pair of numbers to a more general pair of types.
So, let's say, you have a + b
, and a
doesn't know what to do with b
, then within a
's +
method, it will call b.coerce(self)
, and b
will respond with [a_converted_to_a_more_general_type, b_converted_to_a_more_general_type]
, after which a
will retry calling a_converted_to_a_more_general_type + b_converted_to_a_more_general_type
:
class Roman < Numeric
def initialize(str)
# dummy implementation for the sake of this example
@num = 5 if str == 'V'
end
def coerce(other)
puts "`coerce` called with #{other.inspect}, which is a #{other.class}"
[other, @num]
end
def to_int
puts '`to_int` called'
@num
end
end
2 + Roman.new('V')
# `coerce` called with 2, which is a Integer
#=> 7
('a'..'g').to_a[Roman.new('V')]
# `to_int` called
#=> 'f'
Upvotes: 6
Reputation: 160571
If a string contains digits only it can be coerced to an Integer:
Integer('1') # => 1
Integer('1.0') # => ArgumentError: invalid value for Integer(): "1.0"
This can be useful if you're trying to make sure that a value that was input was truly an integer instead of a float or whether it had any other "junk" in it:
Float('1.0') # => 1.0
If you want to coerce values to an integer, and are not trying to validate input, then you probably want to use to_i
instead, which gives you a better chance of getting a usable result without an exception occurring:
1.to_i # => 1
'1'.to_i # => 1
Time.now.to_i # => 1485640961
''.to_i # => 0
nil.to_i # => 0
1.to_i + nil.to_i # => 1
Upvotes: -1