In Progress
Unit 1, Lesson 21
In Progress

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