Traits

One of the changes we’ve made in going from smalltalk to neptune, beyond pure syntax, is in the object model. We’ve added an optional type system and introduced two new concepts: traits and protocols. I don’t mean new in the sense that we invented them, they’re pretty well known actually, but they’re new in the sense that they’re not part of standard smalltalk. In this post I’ll give a brief introduction to traits.

Smalltalk’s single inheritance model is clean and simple, but it also has many limitations. The most obvious limitation is that a class can only share code with one other class, its superclass. In addition, there are also more conceptual limitations. If I create a subclass B of class A, an instance of B should be considered a special case of an instance of A. The typical toy example of this is animals. Let’s say that the class Eagle extends the class Bird which extends the class Animal. This models the fact that an eagle is a bird and a bird is an animal. On the other hand, a bat shares many of the characteristics of a bird but it is not a bird so the class Bat cannot inherit from Bird. Even though there is much code in the class Bird that could be used in class Bat, single inheritance doesn’t allow them to share code because it is just conceptually wrong. I guess this is a pretty poor example but at least if someone disagrees and believes a bat can be implemented by using parts of birds I can just sit back and wait for the villagers with torches and pitchforks to take care of him. But I digress.

C++, another ancestor of neptune, has multiple inheritance which at least solves some of the problem — a class can now share code with more than one other class — but introduces so many other problems that we’ve never really considered it as a serious alternative. The solution we’ve decided to go for is traits. Traits seem to be slowly making their way into the mainstream as a more flexible way of sharing code between classes; variants can be found in languages such as perl6, scala and fortress.

To explain how traits work in neptune I’ll show you an example from our libraries. One place where we’ve used traits in our libraries is for implementing relational operators such as <, >=, etc. In most languages I know, the standard way of making an object x comparable to other objects is to add a method to x that takes the object to compare it with, y, and returns an integer signifying whether x is less than, equal to, or greater than y. In java that method is compareTo: if x is less than y, x.compareTo(y) returns a negative number, if they're equal it returns zero and if x is greater than y it returns a positive number. In neptune, we use the spaceship operator, <=>, for comparing objects. For instance,

"aardvark" <=> "zebra"

returns a negative number because the string "aardvark" is lexicographically less than "zebra". That's all nice and general but it is not very convenient or readable. Writing

if ((x <=> y) < 0) return y;

is a lot less clear than writing

if (x < y) return y;

This is where traits come in. Since the relational operators are completely defined by the comparison operator I can implement the relational operators as a trait:

trait TRelation {

require int operator <=>(var that);

bool operator ==(var that) {
return (this <=> that) == 0;
}

bool operator <(var that) {
return (this <=> that) < 0;
}

// ... and >=, <=, !=, > ...

}

This doesn't define an a class but a set of methods that can be used by any class, provided that is has the <=> operator. One example of this could be an implementation of fractions:

class Fraction {

use TRelation;

int num, denom;

bool operator <=>(var that) {
// ... fraction comparison ...
}

...

}

The easiest way to think of the use declaration is that it essentially copies the methods declared in the TRelation trait and pastes them into the Fraction class. The only requirement in this case is that class using the trait must define the comparison operator. By the way, a class can use as many traits as it wants.

Unlike inheritance, the class Fraction is not put in any kind of "relationship" with the trait TRelation. Traits are nothing more than collections of methods that you can import into your own classes almost as if you had used a #import in C. It should be completely irrelevant to anyone who uses the class Fraction whether the < operator is implemented by using a trait or by writing a method in the class itself. Because of this, a trait cannot be used as a type -- you cannot, for instance, declare a variable of type TRelation. This is also the reason why you write use declarations in the body of the class and not the header, since it is an implementation detail and not something should know about to use the class.

A trait is only allowed to contain methods, not fields. This might seem like a considerable limitation but it simplifies the model considerably and if a trait needs a field it can simply require the class to have it. For instance, this trait which contains methods for dynamically extending an array or vector, needs a contents field:

trait TVector {

require accessor contents;
require accessor contents=(var value);

void ensure_capacity(int new_size) {
if (new_size > this.contents.size) {
// ... enlarge contents ...
}
}

}

TVector does not have the field contents itself but requires any class that uses it to have accessors for it. A class that uses TVector can implement this by actually having a field contents but since accessors are just ordinary methods (you can think of them as getter and setter methods), the class is free to implement them however it wants.

Our traits are very closely modeled on the trait mechanism for smalltalk described in the article linked above (and again here for your convenience) which I can definitely recommend reading. There are plenty of subtleties to the mechanism that I haven't mentioned. What happens if there's a name clash when importing more than one trait? (There's a mechanism for renaming methods) Can traits use other traits? (Yes) How does super calls work with trait methods? (I can't answer that in a single sentence). For the full answers to all this you'll just have to read the article. The original motivation for adding traits was the trouble we've had with implementing the collection hierarchy. It turns out that some of the people who wrote the smalltalk traits article have also written another one on exactly that: using traits in the design of a collection hierarchy.

So far we haven't used them that much in our libraries -- my guess would be that there is at most one trait to every ten classes. But in the cases where we have used them, they've really saved the day.

2 Responses to Traits

Leave a Reply

Your email address will not be published.


*