Ruby Mixins: An Example

Ovid on 2009-07-16T12:04:14

Many people hear about Ruby mixins, but don't actually know what they are since they don't program in Ruby. Plus, I see a number of questions online about how to write a mixin in Ruby. So here's a quick example for Perl programmers to see how it's done (note: I'm hardly a Ruby guru, so be forewarned).

To use mixins in Ruby, first create modules with the behavior you want to share. A module can be thought of as an incomplete class which you cannot instantiate.

module Bomb
    def explode
        puts "Bomb explode"
    end
    def fuse
        puts "Bomb fuse"
    end
end
module Spouse
    def explode
        puts "Spouse explode"
    end
    def fuse
        puts "Spouse fuse"
    end
end

Then in your class, simply include the mixins:

class PracticalJoke
    include Spouse
    include Bomb
end

So what does that do?

joke = PracticalJoke.new()
joke.explode
joke.fuse

This nicely allows classes to simply be responsible for generating instances and shared behavior is handled by the mixins. Unfortunately, that prints:

Bomb explode
Bomb fuse

In other words, we've exchanged multiple inheritance's "first wins" behavior with mixin's "last wins" behavior. Trying to get one method from each appears to need delegation.

Also, I've noticed that it appears that you're not flattening these methods into your class, as you would with roles, but are instead inheriting from the mixins:

irb(main):026:0> PracticalJoke.ancestors
=> [PracticalJoke, Bomb, Spouse, Object, Kernel]

This means that your mixin methods aren't really getting "mixed in" to your class. I'm not sure of the downside of this (aside from having a longer inheritance chain), but the upside is that you can at least override methods cleanly:

class PracticalJoke
    include Spouse
    include Bomb

    def explode
        puts "PracticalJoke explode"
    end
end

joke = PracticalJoke.new()
joke.explode
joke.fuse

And joke.explode will print PracticalJoke.explode. I think this means that if you want a warning at the PracticalJoke level about an accidental override, you're out of luck.

Further comments from Ruby experts welcome.


Why?

joeri on 2009-07-16T15:14:00

Why would you add two mixins with a similar set of methods to one file? Can you think of a practical example ?

A frequent pattern of mixins is adding a certain behavior to different classes, I use this patterm a lot in my apps.

For example, I have module Geocodeable, and I mix it in into other classes like for instance Person and Organization. Voila.

One of the best examples is how Enumerable is mixed in into Array and Hash, adding its cool methods select, reject, inject, partition and so on, and how can easily add the same behavior to your custom class with one line of code.

Re:Why?

Ovid on 2009-07-16T15:49:55

It's easy for this to happen, particularly in large systems. Here's what I'm working on right now:

$ find lib/ -name '*.pm'|grep -v 'Role/'|wc -l
600
$ find lib/ -name '*.pm'|grep 'Role/'|wc -l
42

And we're still adding roles.

When you have a large system with multiple roles, it's likely that you'll eventually encounter situations where roles provide similarly named methods. This should generally only happen if these methods are semantically equivalent, but provide different implementations. As just one example from our code, we have "publication events". Anything which does('Role::PublicationEvent') event gets the entity_identifier() and memberships() methods, but anything which also does('Role::Identifier') must exclude the latter's version of entity_identifier() and memberships() because the behavior is different, even though they're semantically equivalent (actually, memberships is provided via a different role, but this is getting long enough as is).

Since these methods are semantically equivalent, disambiguating these methods via different names would be a bad idea because if we eventually find that we can drop support for one of those conflicting methods (will will for entity_identifier for publication events) and fall back on the original, we don't want to go through all of our code and remove the other method name. That being said, if you have identically named methods which do semantically different things, then you should consider renaming your methods.

As our system grows, we're finding a few more cases like this, but it's vitally important that we have the tools necessary to cleanly resolve this. Mixins don't provide us with that facility.

Re:Why?

joeri on 2009-07-16T16:03:11

Using a thing like the Ruby mixins for such cases is over the top.

The programming God tells me one should use simple delegation for such cases like they do in Cocoa (and it looks simple and beautiful).

Re:Why?

Ovid on 2009-07-16T16:12:28

Delegation is great, but when the thing delegating and the thing being delegated to need to communicate with one another, you start winding up with problems. We already have components (roles) that we can assemble and need to work well together and separating them out via delegation means a separate class heirarchy just have to have something to delegate to. Why would I want to write the scaffolding for delegation? Honestly, roles are really, really simple and make this trivial :)

Plus, delegation is for a has-a relation and that's not what our code is expressing via roles.

Proxies

rfunduk on 2009-07-16T16:11:46

If you _really_ need to mixin two modules that have the same methods you could make a proxy for the first:

http://gist.github.com/148507

Pickaxe Book

Stevan on 2009-07-16T16:25:34

According to the Pickaxe book (figure 18.4) (page 247 of the first edition). Mixins modules are actually proxied. You can see in the diagram that it inserts a Proxy class between Guitar class and the Object class. The Proxy class does not have any methods itself, but instead it has a pointer to the method table of the module it is proxying. This gets more and more ugly and complicated the more modules you mixin, however it does explain the odd "last wins" behavior.

Of course, I am not sure if this is still how it is done, after all the first edition of the Pickaxe book is almost 8 years old now.

- Stevan

Mixins and Inheritance

djberg96 on 2009-07-23T12:25:38

This blog article from Chad Fowler may interest you:

http://www.chadfowler.com/2009/7/8/how-ruby-mixins-work-with-inheritance