Define Conversion Protocols in Ruby

[boilerplate bypath=”confident-ruby-excerpt-preamble” class=”boilerplate preamble”]

The problem

You need to ensure inputs are of a core type with context-specific extra semantics.

The approach

Define new implicit conversion protocols mimicking Ruby’s native protocols such as #to_path.

Explanation

Ruby defines a number of protocols for converting objects into core types such as String, Array, and Integer. But there may come a time when the core protocols don’t capture the conversion semantics your apps or libraries need.

Consider a 2D drawing library. Points on the canvas are identified by X/Y pairs. For simplicity, these pairs are simply two-element arrays of integers.

Ruby defines #to_a and #to_ary for converting to =Array=s. But that doesn’t really capture intent of converting to an X/Y pair. Just like the #to_path conversion used by File.open, even though we are converting to a core type we’d like to add a little more meaning to the conversion call. We’d also like to make it possible for an object to have a coordinate conversion even if otherwise it doesn’t really make sense for it to have a general Array conversion.

In order to capture this input requirement, we define the #to_coords conversion protocol. Here’s a method which uses the protocol:

# origin and ending should both be [x,y] pairs, or should
# define #to_coords to convert to an [x,y] pair
def draw_line(start, endpoint)
  start = start.to_coords if start.respond_to?(:to_coords)
  start = start.to_ary
  # ...
end

Later, we decide to encapsulate coordinate points in their own Point class, enabling us to attach extra information like the name of the point. We define a #to_coords method on this class:

class Point
  attr_reader :x, :y, :name

  def initialize(x, y, name=nil)
    @x, @y, @name = x, y, name
  end

  def to_coords
    [x,y]
  end
end

We can now use either raw X/Y pairs or Point objects interchangeably:

start    = Point.new(23, 37)
endpoint = [45,89]

draw_line(start, endpoint)

But the #to_coords protocol isn’t limited to classes defined in our own library. Client code which defines classes with coordinates can also define #to_coords conversions. By documenting the protocol, we open up our methods to interoperate with client objects which we had no inkling of at the time of writing.

[boilerplate bypath=”confident-ruby-excerpt-postamble”]

5 comments

  1. I’m not sure I understand why using #to_coords is useful here. If we decide to represent points as Point objects, wouldn’t we change every reference to use its #x/#y protocol?

    1. 1) The Point class is one possible future; not a certainty; and
      2) Even if we DO decide to start using points, that’s a pretty big upheaval in an established project to go through and change everything; especially if we aren’t sure this “Point” thing is a good idea yet; and finally…
      3) What if there’s lots of client code assuming that our API will both receive AND return (or call-back with) x/y pairs?

      1. In that case, why not ask start.respond_to(:x) && start.respond_to(:y) instead, and make the interface smaller?

        1. I go into great detail on that question in another section of the book 🙂

          In a nutshell, code that constantly asks #respond_to? is horrible and completely defeats the purpose of duck-typing. Using respond_to? to check for a conversion method is a useful compromise with an important difference: it’s not asking “Do you support interface X”, but rather “can you give me something that supports interface X”. It adds a level of indirection which means that no matter how big interface X becomes, only one #respond_to? check will be needed.

Comments are closed.