Last week, I made a change to the Rust compiler that I’ve wanted to make for over a year. I was so happy about finally getting this patch in that I couldn’t resist a “Woohoo!” in the commit message. My excitement is due to default methods finally becoming a stable feature of Rust’s trait system.
If you want a good laugh, listen to this talk I gave a year ago, at the end of my second summer working on Rust, in which I promised that default methods were coming to the language “next week” and would surely be in the then-upcoming release 0.4. I gave that talk on a Thursday afternoon. On the following Monday, I got the first Rust program with a default method to compile, run, and produce the result it should have. It was pretty cool.
Unfortunately, default methods were still basically unusable for real programming for a host of reasons, and certainly not ready for Rust 0.4. It took another year of work by a number of people, particularly Sully, to get them to where they are today. Release 0.5 added default methods as an experimental, off-by-default feature, and various improvements landed over the summer in 0.7. Finally, with 0.8 on the way, default methods seem to be at the point where we can enable them by default.
So, what are default methods, and why am I so excited about them?
Since default methods are a part of the trait system, I’ll start by saying a bit about how traits work in Rust as of 0.7, the current release. The Rust 0.7 tutorial section on traits covers some of the same ground.
As a first example, let’s look at the ubiquitous
Eq trait, which Rust provides in the standard library as
std::cmp::Eq. You can go look at the code for yourself, but as of release 0.7, it looks like this:
1 2 3 4 5 6
Eq contains signatures for methods named
ne. An actual implementation of the
eq method — which is not provided here — would take two arguments of a given type and return a value of type
bool, presumably after comparing its arguments for equality. Meanwhile, an implementation of
ne would presumably compare its arguments for inequality — but more on that in a bit.
What good does this
Eq trait do us? By itself, not much, so let’s first write an implementation of the
Eq trait for a type whose values we would like to compare for equality. To return to an old favorite example1, let’s suppose that we’ve defined a data type called
Color with four variants:
We can write a function that compares two
Colors for equality, giving it a signature that matches the signature for
eq given in the
1 2 3 4 5 6 7 8 9
eq takes both its arguments by reference2, we dereference them with
* before pattern-matching against them, and the rest is pretty straightforward.
By putting this definition of
eq inside the appropriate
impl declaration, we can make it part of an implementation of
Eq for the
1 2 3 4 5 6 7 8 9 10 11
Let’s try writing a complete Rust program using the code we’ve written so far.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
In Rust, the
== comparison operator is just syntactic sugar for the
eq method on the
std::cmp::Eq trait. So, we should now be able to compare
Colors for equality using
==. In particular,
cyan == yellow ought to evaluate to
cyan == cyan should evaluate to
Unfortunately, the above program doesn’t quite compile under Rust 0.7:
1 2 3 4 5
The compiler’s complaint is that we failed to provide an implementation for
ne, the inequality method. Since
Eq has both
ne method signatures, we’ll need to update the
impl we wrote to include
ne as well.
Happily, this is easy to do: two
ne exactly when they are not
eq, so we can just write a one-liner
ne by calling
eq and then negating the result.
The complete program now looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
It compiles successfully, and running it produces the result we’d expect:
1 2 3
What’s the point?
Let’s look at a situation where having an
Eq trait comes in handy. Suppose we wanted to write a generic function
member that takes an argument
elem of some type
T, and a vector
vec of elements of type
T, and tells us whether
elem appears in
vec. Here’s a first stab at writing a version of
member that is parametric over
1 2 3 4 5 6 7 8 9
Incidentally, the syntax
for vec_elem in vec.iter() works against head-of-tree Rust (and will, I think, be standard in 0.8), but to get this code to compile against Rust 0.7, we have to write
for vec.iter().advance |vec_elem| instead:
1 2 3 4 5 6 7 8
Syntax infelicities aside, this definition of
member looks more or less like what we want. But, it fails compilation:
1 2 3 4 5
What went wrong here? The problem is that, as it stands,
member is polymorphic over any type
T. Since we don’t know anything at all about what
T is, we can’t very well call
== on elements of it, since, after all, we have no idea if it makes sense to compare elements of
T using equality. The compiler conservatively assumes that
== cannot be applied to elements of
T, and raises an error.
In fact, if we wanted to, we could derive a so-called “free” theorem about the type of
member, which would say that it is impossible for any function of
member’s type to make an equality comparison over elements of
T (without breaking type safety).
Well, that’s certainly inconvenient.
Eq trait can help us here. Instead of parameterizing
member over any
T, we can limit it to only those types on which we know
== can be called — that is, the types for which the
Eq trait is implemented. That is, traits give us a way to express bounded quantification in Rust. All we have to do is make a small tweak to the type signature of
1 2 3 4 5 6 7 8 9 10
The compiler is happy with this updated definition of
member. Here’s a complete program that uses it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
This version of the code compiles without incident, and since
magenta is indeed a member of
~[black, yellow, magenta, black], it runs and prints
true, as we’d expect:
1 2 3
As handy as
Eq is, there’s something unsatisfying about what we’ve done so far. Recall that our first attempt at a program that implemented
Eq for the
Color type failed compilation under Rust 0.7, because we forgot to implement the
ne method. To make the compiler happy, we had to add a definition of
ne to the implementation of
But this definition of
ne looks tantalizingly generic. The body of
ne refers only to the already-implemented
eq method and its own two arguments. Except for one little occurrence of
&Color in the type signature, nothing about
ne is specific to the
Color type as such.
Wouldn’t it be great if, instead of having to implement a version of
Color — as well as another version of
ne for every type whose elements we want to be able to compare for equality — we could just write a single, type-generic version of
ne and use that everywhere? This is, in fact, what default methods let us do.
Earlier, we saw that in Rust 0.7, the implementation of
std::cmp::Eq looked like this:
1 2 3 4
But let’s see what
std::cmp::Eq looks like these days:
1 2 3 4
What happened with
ne? It’s no longer merely a signature; now, there’s some code, right there in the trait itself! Moreover, it looks like exactly what we had to write in our own implementation of
Color — except that now, its signature is type-generic. In other words,
Eq now provides a default implementation of
ne, known as a default method.
Because of this change, our original program, the one without an implementation of
ne, will now compile as written! The compiler automatically specializes the default implementation of
ne to the
Color type, so we no longer have to implement
ne ourselves. Furthermore, if we have lots of
Eq, we’re spared having to write boilerplate versions of
ne in all of them. And if we felt like adding a new signature to the
Eq trait, we could just throw in a default method for it, then gradually add specialized versions of the method to individual
impls, instead of having to update every
impl just to get our code to compile again.
You can try the code out yourself in this Rust playpen, which lets you choose whether you want to compile a program against Rust 0.7 or a snapshot of the master branch — try both and compare the results. You can do the same with our program that uses
member — it should compile and run fine under “master” with the
ne implementation left out, modulo the
for ... in ... syntax change.
A case study for default methods
At the beginning of last summer, the Rust type inference system had been recently overhauled by Niko. It made heavy use of the then-still-rather-new
impl language features, which hadn’t existed the summer before that. I spent a lot of time puttering around in the type inference code, which was one of the more well-documented parts of the compiler. After all, who can resist ASCII-art docs?
Anyway, at one point during type inference, the compiler performed “type combining”. Without going into what that means, the implementation defined an
iface (short for “interface”, and a precursor to
combine (lower-case was the convention back then), and there were
combine for the three “type combiner” data types, called
sub. All three
impls were required to implement all of the method signatures in the
combine trait, even though some of the implementations were identical in two (or in all three!) of the type combiners. For example,
combine included a signature called
modes whose implementation was the same in all three of the
impls. At the time, the code looked something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
This year-old code is not even close to being legal Rust anymore, but hopefully, it conveys something about what was going on. Because it wasn’t possible to write down a default method implementation for
modes in the trait itself, the workaround was to define a separate out-of-line method
super_modes, and have each
super_modes. We had to use the same workaround for lots of other methods in
combine, as well.
The code was filled with plaintive comments from Niko about how the implementation could have been so much cleaner if only we had something like default methods. In fact, it was this code that convinced me that default methods weren’t some exotic wishlist feature, but something that Rust really needed, and when I wrote up my proposal for default methods last summer, I used the type-combining code as my motivating example.
Once default methods were working, I was finally able to refactor the type-combining code to use them. That’s the reason it took me over a year to put in that pull request — I had to wait until we had default methods! Once we did, though, it was an easy change; I just removed the
super_* methods, replaced them as methods on the (now-capitalized)
Combine trait instead, and got rid of all the boilerplate methods in the
impls. The best part is that my patch removed 217 lines of code, which I think is a nice demonstration of the power of default methods.
From time to time, people ask me how they can start contributing to Rust. The answer depends on the individual, but if your motivations are anything like mine, I think default methods make available a fair amount of low-hanging fruit. For example, it looks like there are unnecessary
ne methods in various
Eq, just hanging out in the standard library, ripe for the deleting.
Although in some cases it makes sense to explicitly implement another version of a method that has a default implementation — say, for performance reasons — falling back to the default method is perfectly reasonable in other cases. Give it a try and see — negative diffstats may await you!
Colorexample is adapted from one in the chapter on typeclasses in Real World Haskell, by Bryan O’Sullivan et al. If you’re familiar with typeclasses, you might find that traits look familiar, too, if you squint a bit. ↩