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.
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
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
1 2 3 4 5 6 7 8 9 10
Now let’s implement the
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.
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
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
trait_send. That allows us to compose all implementations, ignore
some, or do whatever we want with them.