Double Dispatch in Ruby with Refinements
Refinements are a relatively new feature of Ruby. They’re really cool, but at the time of writing I couldn’t find anything exciting that people were doing with them, so here’s a suggestion:
A thing that Ruby seemingly lacks is some easy and contained way of implementing double-dispatch. This is not surprising, support for double-dispatch isn’t common in duck-typed languages, but refinements can help us out. They allow us to monkey-patch methods on existing classes in a safe way; the monkey-patched methods can only be called in a scope where they are activated.
First we’ll look at what double-dispatch is, then we’ll look at how to implement it using classical Ruby structures and how to do this using refinements.
Just to be clear on the problem domain: Dispatch refers to selecting a method implementation. It is the asking of the question:
What chunk of code should I run when a certain message is sent to a certain object?
Single dispatch is therefore selecting a method implementation based on the type of one object; Think about the difference between
human.fly(). The former would just have the
bird take off. The latter would necessitate the
human finding their way to an airplane or a cliff. In this case there are two implementations of the
fly() method. We select which one we want to execute based on the type of object we call the method on.
Double dispatch is then selecting a method based on the type of two different objects. This is like calling
thing_that_eats.eat(coffee) and having the person object know to chew the biscuit, but not the coffee.
This is hard in a duck-typed language because methods are identified by which class they are called on as well as their arity, rather than the type of their input arguments. Doing the following in Ruby would just overwrite the first eat method, and the
thing_that_eats will choke to death when it encounters an enticing biscuit:
In order to save the
thing_that_eats you would have to know what you’re feeding it, and call methods like
thing_that_eats.eat_liquid_food(soup). Alternatively, you can pass any type of food to
eat and have the object interrogate the food’s class so it can delegate to a specific method to handle that food-group. Various influential people seem to dislike type-checking objects like this, though.
So how can we avoid a gastronomical crisis without re-engineering the whole scenario? A solution (the one we’ll stick with to keep this narrative going) would be to have the interacting objects work together.
Having objects work together to implement double dispatch involves passing control between them in order to eventually find the right method implementation. Consider the following:
Notice how the control flows from
biscuit and back to
eat is called on
biscuit as an argument.
thing_that_eatshowever, has no idea that
biscuit is a
CrunchyFood and that it needs to chew. To remedy this,
thing_that_eats assumes that whatever is passed to it in this method has a
be_eaten_by_thing_that_eats is called on
self) as an argument. At this point
biscuit can make a safe assumption that what is passed to it is an instance of
ThingThatEats(its right there in the method name!) and it obviously knows that it, itself, is an instance of
Biscuit. Given all this information,
biscuit can go ahead and tell
eat_crunchy_food on itself.
And there we have it: Double Dispatch. We’ve selected a method implementation based on (1) the Class of object the method was called on, and (2) the Class of argument that was passed to the method.
But what about the open-closed principle you say? What happens if I don’t have access to the source code of
LiquidFood and I need to implement
thing_that_eats.eat_liquid_food? What if
SpoiledFood is so bloated that adding any more code like
SpoiledFood.be_eaten_by_thing_that_eats to it makes you violently ill?
So we’d like to be able to define an implementation of
ThingThatEats that specifically operates on
LiquidFood. We don’t want to change the
LiquidFood class because it would be undesirable if this new code affected the rest of the codebase. Fortunately Ruby has refinements!
Imagine the following:
Here we define the LiquidFood’s necessary instance methods in a refinement which we activate inside the ThingThatEats class, and Blam-o!
LiquidFoodis completely ignorant of
ThingThatEats can have an implementation of the method that is specifically designed to operate on
LiquidFood, and potentially another one for
MushyFood etc. Just like if double dispatch was actually supported.
Now would be a good time to extract
CrunchyFoodthat we defined earlier. Stick it inside a refinement to
CrunchyFood in the
FoodRefinery so we can keep all these special methods that will only ever be called from one context in one place.
So why is this cool? Two reasons:
- It allows us to isolate very specific code from the rest of the codebase and put all of the different implementations for different classes together.
- It’s a pretty cool alternative to doing type-checking on objects.
Be careful about doing this in a situation where you would be
using the refinements module in a lot of places, though. Developers who aren’t familiar with our codebase may respond to a
NoMethodError coming from
MushyFood by implementing
MushyFood rather than in the refinements module.