Implicit Conversion
Video transcript & code
The other day we added explicit conversion operators to our Feet
class. This enabled clients to convert Feet
to raw floats or integers using the conventional #to_f
and #to_i
messages. It also enabled Feet
objects to play nicely with string formats.
require "./feet"
len = Feet(1234.5)
len.to_f # => 1234.5
len.to_i # => 1234
"The bridge is roughly %d feet long" % [len]
# => "The bridge is roughly 1234 feet long"
Having just talked about explicit conversion methods, now seems like a good time to introduce implicit conversion methods. So that's what we'll talk about today.
First, let's define some terms. What exactly do we mean by "explicit conversion method" and "implicit conversion method"?
Let's say we want to convert a floating point number to an integer. One way to do it is to send #to_i
. Another way to do it is to send #to_int
. Both return a truncated version of the original number.
123.4.to_i # => 123
123.4.to_int # => 123
So why do both of these methods exist?
The first method, #to_i
, is probably the one you're most familiar with. It is what as known as an explicit conversion method. Explicit conversion methods normally follow this naming pattern: to_
, followed by a single letter standing for the target type. Other examples include #to_s
to convert to a String
, and #to_a
to convert to an Array
. As you are no doubt aware, these one-letter conversion methods are very common in Ruby core classes.
123.4.to_s # => "123.4"
{x: 23, y: 32}.to_a # => [[:x, 23], [:y, 32]]
The term explicit conversion method refers to how Ruby uses these methods. Or more precisely how it doesn't use them. Because while Ruby provides methods to change a float to an integer, a hash to an array, and just about anything to a string, there are very few places where Ruby or its core libraries use these conversion methods. They are called explicit because they exist for us, the programmers, to explicitly request a type conversion. With rare exceptions Ruby doesn't call them for us.
This is in line with Ruby's general policy of not doing "automatic" conversions behind the scenes, the way many other programming languages do. These automatic conversions can be convenient, but they are also a frequent source of surprising behavior. For the most part Ruby prefers not to make assumptions about what type we meant to use.
But there are exceptions. In Episode 206, for instance, we learned about Ruby's numeric coercion mechanism, which enables this code multiplying an integer by a rational number to work.
10 * Rational(2,3) # => (20/3)
The other major exception has to do with implicit conversion methods.
Let's say we have an API to a magical astrological device, which can deliver a batch of 12 fortunes, one for each birth-month. For mystically inscrutable reasons, the format of the data returned by this API is a 12-element array. The indices are zero-based month numbers, so 0 is January and 11 is December. The values are strings.
def fortune_o_matic
Array.new(12){ `fortune` }
end
I was born in July, so to get my fortune I access the element of the array with the zero-based index of six.
require "./fortunes"
fortunes = fortune_o_matic
fortunes[6] # => "You have taken yourself too seriously.\n"
This isn't the most self-documenting code in the world. As an aid to using this API, we create a new Month
class, which encapsulates the name of a month and the month's index in the fortunes array. We also give it an explicit #to_s
conversion method which returns the month's name, and a #to_i
method which returns the index. We then proceed to define 12 constants, one for each month.
Month = Struct.new(:name, :index) do
def to_s
name
end
def to_i
index
end
end
January = Month.new("January", 0)
February = Month.new("February", 1)
March = Month.new("March", 2)
April = Month.new("April", 3)
May = Month.new("May", 4)
June = Month.new("June", 5)
July = Month.new("July", 6)
August = Month.new("August", 7)
September = Month.new("September", 8)
October = Month.new("October", 9)
November = Month.new("November", 10)
December = Month.new("December", 11)
We can now write code that reads a bit better. Instead of magic numbers, we can use named month objects.
require "./fortunes"
require "./month"
require "./months"
birth_month = July
fortune = fortune_o_matic[birth_month.to_i]
puts "I was born in #{birth_month}. My fortune is:\n #{fortune}"
# >> I was born in July. My fortune is:
# >> Truth will out this morning. (Which may really mess things up.)
This is better. But since the Month
objects are basically just a wrapper around an integer index, it would be really cool if we could use them as array indices directly, instead of having to convert them to an integer first. Right now if we try that, we get a conversion error.
require "./fortunes"
require "./month"
require "./months"
fortune_o_matic[July] # =>
# ~> -:5:in `[]': no implicit conversion of Month into Integer (TypeError)
# ~> from -:5:in `<main>'
Note what the error says: "no implicit conversion of Month into Integer". This is a clue to what we can do to make this code work.
We go back to the definition of the month class, and add an implicit integer conversion method. The name Ruby expects for an implicit integer conversion is #to_int
. Since we already have a #to_i
method which does what we need, we just alias it to #to_int
rather than writing a new method.
Month = Struct.new(:name, :index) do
def to_s
name
end
def to_i
index
end
alias_method :to_int, :to_i
end
Now we can use our month objects as array indices, without any explicit conversion! Month objects are not integers, but now we can use them as if they were.
require "./fortunes"
require "./month2"
require "./months"
fortune_o_matic[July]
# => "Hope that the day after you die is a nice day.\n"
Why does this work? Deep down in the implementation of the Array subscript operator, Ruby does some optional type conversion on the index argument. If the index is not an Integer, and if it responds to the #to_int
message, Ruby will send it the #to_int
message and use whatever results as the actual array index value. So by adding the #to_int
method to our Month
class, we've effectively given Ruby both the permission, and the means, to treat Months
as interchangeable with integers.
As it turns out, this is not an isolated circumstance. These implicit conversions are everywhere in the Ruby core classes and standard libraries. Just about anywhere you see a Ruby core method documented as taking an integer argument, chances are it will also accept a non-integer argument that responds to #to_int
.
As one random example just to show how pervasive these conversions are, take the Regexp#match
method. It accepts a string to match against, and an optional second argument which is an integer specifying what position in the string to start matching.
With a starting position of zero, this regexp matches the first word in the string. With a starting position of 6, it matches the second word.
With a starting position of July
, it also matches the second word, because July
is implicitly convertible to the number 6.
This is nonsense code, of course. But it goes to demonstrate how these implicit conversions can be found everywhere.
require "./month2"
require "./months"
rx = /foo\w*/
str = "foobar foobaz"
rx.match(str, 0)
# => #<MatchData "foobar">
rx.match(str, 6)
# => #<MatchData "foobaz">
rx.match(str, July)
# => #<MatchData "foobaz">
With Ruby willing to implicitly convert arguments all over the place, you might be wondering how it avoids the surprising accidents that often accompany default conversions in other languages. The answer is simple: while Ruby allows for these implicit conversion, it provides very, very few implicit conversions out of the box. Methods like #to_i
and #to_s
may be commonplace, but methods like #to_int
are extremely rare. For the most part it's explicit conversions or nothing.
However, as we can see from our fortune example, we can write our own classes to take advantage of these implicit conversion protocols. So, returning to our Feet
class, we ask ourselves: given that we have a #to_i
explicit conversion method, do we also want a #to_int
implicit method? After all, we could view a Feet
object as just a glorified wrapper around a numeric value.
Implicit conversion methods exist to make objects behave interchangeably with a core type. Our rationale for creating the Feet
class in the first place was to have a type which was not interchangeable with core numeric types, except under certain controlled circumstances, in order to avoid unit mismatches. When we view it in this light, we realize we have our answer: it doesn't make sense for Feet to be implicitly convertable to any core types. We'll leave it as it is.
To sum up: Ruby supports the idea of both explicit and implicit conversion methods. Explicit conversions are common, but they usually need to be deliberately invoked; Ruby won't invoke them for us. Implicit conversion functions, by contrast, are rare; and Ruby will automatically use them when it finds them. We can equip our own classes with implicit conversion functions to, for instance, produce objects which can be used interchangeably with integers. However, this is a technique that should be used sparingly and with careful consideration.
And that's it for today. Happy hacking!
Responses