A revival of my blogging habits, and a new beginning playing with Ruby after years in .net.

Tuesday, November 25, 2008

Traits in Ruby

(This post is copied from my old blog, which has disappeared from the internets by and large. So it contains thoughts that I'm not currently thinking, and which have largely been superseded in these modern times. This was published Sat Feb 14, 2004.)

In a post to ruby-talk, Daniel Berger asked about any discussion of traits. Matz weighed in with the opinion that " "module" in Ruby and "trait" are very similar idea. " (Matz, ruby-talk 92858). Not content to let that rest, I looked up the original paper, and found some food for thought. So I've worked up something of a Ruby implementation of Traits, as presented by the paper.

Briefly, traits are proposed by the authors of the paper as a primitive unit of code reuse. Classes should be constructed by composing traits, and viewed normally as a receiver of messages, that is, relating normally to other classes. According to the paper, traits have the following properties:

A trait provides a set of methods that implement behaviour.

A trait requires a set of methods that parameterize the provided behaviour.

Traits do not specify any state variables, and the methods provided by traits never directly access state variables.

Traits can be composed: trait composition is symmetric and conflicting methods are excluded from the composition.

Traits can be nested, but the nesting has no semantics for classes - nested traits are equivalent to flattened traits.

Superficially, it appears that modules do have these properties. But the paper does address the problems of mixin inheritence. These are total ordering: where mixins are added linearly to a class, one at a time, with later mixins overriding all named features of earlier mixins; a lack of control from the composite entity about the composition of mixins, occasionally requiring tricky code to obtain the final topology of the composed class; and fragile heirarchies: the proposition that a change to any module may cause silent changes and overrides to the composed class.

The example implementation of traits is done in Squeak. Over the course of my recent work, I've become almost conversant in the language, so I'll attempt to translate their example into Ruby. The example given is the ol' shape drawing trick. I hope nobody mentions anything about rectangles, squares, chickens or eggs in this thing. The TDrawing trait, in Ruby, becomes:

Note that the requirements for the mixin are expressed only as comments. This is similar to the Enumerable mixin problem - it requires a hook into #each to get it going, but this is only expressed in docs, or at runtime when you call an Enumerable method on an Enumerable descended object without #each. There are other rules to traits, such as glue methods, that need to be respected, too. Is it possible to trivially extend Ruby to handle a trait style composition? I'll start with the trivial requirement: requirements.

Normally, I'd probably consider testing to be sufficient, to pick up the lack of hook method, rather than this quasi-static typing hack. But I'm just trying to stay faithful to the paper here, and it does have some "hard-documentation" style elegance. I'm going to tiptoe around the static typing thing, and move on to the next requirements: Class methods take precedence over Trait methods, and Trait methods take precedence over superclass methods. Time to write up some unit tests:

The code needs some tidying, obviously, but it passes the tests. Moving on again. The paper describes Nested Traits, allowing Traits to be composed of other Traits. Nested traits are nasty, because if you include a Trait into another Trait, you need to avoid checking for requirements. Its only when a Trait is composed into a class should the sum of all requirements be added. I can test this with the following additions to the code:

Passes the tests, although under the glaringly poor assumption that all Modules are Traits. I should really test for that foreseeable problem, but I'm going to ignore it for now. Next up: combining required_methods, and thinking of a better name for them. Add the following tests:

The next hurdle is Conflict Resolution. By default, mixins in Ruby overwrite existing mixed in methods in the order of their inclusion. Traits are specified to allow aliases and method removal on inclusion: the Squeak code looks like:

Even though this is a major part of the whole Traits business, I'm going to completely ignore its implementation right now, because I've done enough to form a few opinions on the subject, and I'm getting tired. I'd probably have to alias and override Object#extend and Module#include, or (better) write Object#extend_trait and Module#include_trait, to implement Conflict Resolution. I'll leave this as an exercise for the reader.

This hacked up implementation of Traits is a proof-of-concept that Traits can fit reasonably easily into Ruby, using straight Ruby without extensions. Without implementing anything, its possible to code in a Mixin-Oriented style in Ruby - composing classes from small, well-defined modules, paying attention to inclusion order and the names of methods across all of the mixins one includes. Developing Traits would make this informal, possibly difficult to scale process quite a bit easier - resolving clashes and ensuring, when a class is parsed, that it meets requirements for using a Mixin (failing early).

Despite that, I'm still not sold on the Traits concept. I'm not sure that sprinkling methods over a distributed set of Mixins would be fun to maintain, on a large scale. The Squeak implementation took advantage of the browser: you only ever see one method at a time in Squeak (smalltalk) code, anyway, and if the browser integrates with Traits, you could view and edit the methods as "part" of the class anyway. With Ruby, without a great IDE, you'd need to flick across files and use a bit of imagination to get a good "view" of your composed class - to even see what methods it's composed of. I believe that traits lose a good deal of their appeal without the Smalltalk browser, or a great Trait-aware IDE.

The final version of the Trait module, along with its tests, is presented below, if anyone cares to take it further. As a postscript, there's a bit of module magic code, far more mature than this, out there now: import-module, by Shin-ichiro HARA and aliasing_module by Nobu (which seems to handle the implementation of aliasing I neglected above).