Define Conversion Protocols in Ruby

This article is an excerpt from my in-progress book, "Confident Ruby".

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.

If you enjoyed this excerpt from Confident Ruby, why not get the whole book? Early access is available now!
This entry was posted in Ruby and tagged , , . Bookmark the permalink.
  • Peter Jaros

    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?

    • http://avdi.org Avdi Grimm

      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?

      • Peter Jaros

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

        • http://avdi.org Avdi Grimm

          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.

          • Peter Jaros

            Interesting. I should probably go read the book then. :)