While doing some housekeeping on my blog over the weekend, I can across an ancient post by Patrick Walton. While I didn't realize it at the time, this post embodies what has become one of my core principles in program language design.1 In re-reading Patrick's post, this quote stood out in particular:

Language design tends to go in cycles: we grow the language to accommodate new functionality, then shrink the language as we discover ways in which the features can be orthogonally integrated into the rest of the system. Classes seem to me to be on the upward trajectory of complexity; now it’s time to shrink them down. At the same time, we shouldn’t sacrifice the functionality that they enable.

This cycle of growing and shrinking as a key part of the process in the early days of Rust. Upon reading this section, I found myself asking "how could we shrink Rust today?"

What happened to classes?🔗

To be honest, I had forgotten Rust had classes at one point. I remembered resources and objects, but forgot we had a brief window where there were classes. Patrick's post explains what happened to them. Essentially, once we added classes and a bunch of other features, we realized that classes combined five features that we could implement independently in a way that's more general. These, along with their modern replacements in Rust, are:

  1. Nominal records, replaced by struct.
  2. Constructors, replaced by struct literal syntax and plain functions that are conventionally called new.
  3. Field-level privacy, replaced by module-level privacy.2
  4. Attached methods, replaced by inherent impls.
  5. Destructors, replaced by Drop trait.

Some of these features weren't so much replaced as removed. For example, it's hard to claim Rust has constructors today, other than by convention. Similarly, if I remember right, at the time Rust also had the struct keyword, so you used struct if you just wanted a nominal record or class if you wanted the rest of these features. Or in the case of field-level privacy, we basically just decided this feature wasn't necessary.3

For the two features that had a clear replacement, by decoupling them from classes we gained a lot more power. You can attach methods to any type now, like enums and even primitive types, not just classes. Destructors are much simpler now too, since you implement Drop just like any other trait.4

The end result of this was we replaced a large feature, classes, with a handful of smaller, orthogonal features. The result was something that composed better5 and gave us more power and flexibility.

What does this have to do with Rust today?🔗

To me the key take away, at least looking back from over a decade later, is that a big part of why Rust is the way it is today is that we were able to add a bunch of features and then pare them down once we got some experience. In Rust's history, it's had three different ways to do destructors, and while I don't recall exactly, I suspect at least two of these coexisted at some point.

It's somewhat harder to follow this model now. In the early days, we made breaking syntax changes sometimes multiple times a week.6 At that time, the Rust team was a handful of people, about as many interns, and some people who hung out on IRC. Today the community is much larger and people are using Rust in mission-critical projects where they can't afford to make weekly syntax updates. And of course, Rust 1.0 came with a promise that there would be no more breaking changes. You can can rely on Rust to keep working tomorrow.

Rust is still able to grow, but shrinking is much harder, and as a result, we have to be much more conservative in how Rust grows. We have some ability to shrink through the editions system, but this is still not a great mechanism for rapidly iterating on designs.

Anyway, I don't really have a solution, or even necessarily a clearly defined problem. I mostly just wanted to observe that developing Rust is harder today because we mostly have to look at things incrementally. It's much harder to design a set of interrelated features that maybe by themselves wouldn't be particularly noteworthy but together are quite powerful.

Fortunately, Rust does have the nightly compiler, and a process for experiments. That seems like the right environment to do the kind of language experimentation today that was possible in the early days. This is the same codebase that becomes the stable compiler, so we still need to emphasize stability and maintainability, but liberal experimentation in the nightly compiler with many different Rust features at once seems like it has the possibility to do the same kind of broad scale language iteration that we did in the early days while staying true to Rust's stability promises.


1

I've since started calling this my Spiky Blog Theory of Programming Languages, but it deserves a post of its own.

2

One way of looking at this is that classes included their own module or namespace, and this was seen as unnecessary complexity.

3

It might seem nice to be able to make fields on a struct private today, but that requires us to pull in an number of other features. In particular, you need some methods that you can make public which do have access to the private fields. That's why there were attached methods before, and something like that could work with impls but it would be tricky since impls are a lot more flexible.

4

Early Rust had resource types which were basically a wrapper around a type that included a destructor. In some ways it was nice because most things didn't have destructors, but it also meant when you needed one you had to put your code through some contortions to make it work well with an attached destructor. Also, while it's tempting to say Drop is just like any other trait, it's not really because it has special meaning to the compiler.

5

I expect had we kept classes it'd be common to have classes that just wrap an enum, since otherwise we wouldn't have had a way to attach methods to enums. Eventually we probably would have invented some kind of enum class syntax.

6

This is a big part of why rustfmt is so good, because that was how we rewrote the whole compiler every time we had a major breaking syntax change, which was not uncommon.