Its a refine-ary. Don’t look at me like that.

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 bird.fly() and 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(biscuit) and 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_crunchy_food(biscuit) and 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 thing_that_eats to biscuit and back to thing_that_eats:

eat is called on thing_that_eatswith 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 method.

be_eaten_by_thing_that_eats is called on biscuit with thing_that_eats (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 thing_that_eats to 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 eat on 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, yet 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 be_eaten_by_thing_that_eats from 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:

  1. 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.
  2. 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 be_eaten_by_thing_that_eats on MushyFood rather than in the refinements module.