Refinements are the most buzzed new feature in Ruby 2.0. Admittedly, they’re probably a bad idea. But honestly I couldn’t resist trying them to implement traits!
What are traits?
Traits are like Ruby modules in the sense that they can be used to define composable units of behavior, but they are not included hierarchically. They are truly composable, meaning that are pieces that must either fit perfectly or the host object must provide a way for them to do it, normally resolving conflicts by explicitly redefining the conflicting methods.
Since I first read about traits, I found them better than Ruby mixins, that’s why I implemented them natively in Noscript, my programming language running on the Rubinius VM. But having traits in our beloved Ruby turned out to be less trivial than expected.
A while ago I tried to implement traits with pure Ruby and gave up. The problem basically was the way in which a Ruby module is included in a class or extended in an object. One of the power features of traits is the explicit conflict resolution between conflicting implementations of the same method, and that turned out to be a pain in the ass with modules, so I gave up for a while.
Introducing Traitor
So when I heard that MRI 2.0 had a release candidate with refinements, I thought: well let’s give it a try. FUN!!!
And so I did! Traitor is the result. Let’s see how it works:
Let’s say we want to have Rectangle objects that have color and shape. Those
two behaviors will be composed as traits, let’s see Colorable:
1 2 3 4 5 6 7 | |
Easy. For now, Colorable only knows how to compare itself to other
Colorable objects. Let’s try and use it from Rectangle:
1 2 3 4 5 6 7 8 9 10 | |
Now let’s implement the Shapeable trait:
1 2 3 4 5 6 7 | |
Shapeable knows how to compare itself to other Shapeable objects, through
the number of sides that it has.
Our Rectangle needs to be both, the problem is that if we use both traits,
since they have no hierarchy, a rectangle won’t know how to respond to #==.
What implementation should it use, the Colorable or the Shapeable? No way of
knowing. When in doubt, Rectangle will always raise a trait conflict error:
1 2 3 4 5 6 7 8 9 10 11 12 | |
Resolving conflicts explicitly
We must provide a mechanism to resolve the conflict in Rectangle, our host
class. Fortunately, it is as easy as defining our own version of #==:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Now a Rectangle knows how to compare itself to other rectangles, via both its
shape and color.
The cool thing is that we have granular access to any implementation of our
traits via trait_send. That allows us to compose all implementations, ignore
some, or do whatever we want with them.