Why Like Rust?

By | November 16, 2022

For the last several years, programmers keep choosing Rust as their favorite language. I haven’t seen a whole lot of explanation of why. After a conversation with my manager, I realized that my reasons might be quite a bit different than others.

Why Learn a New Programming Language?

One of my favorite computer science quotes is:

A language that doesn’t affect the way you think about programming, is not worth knowing.

Alan Perlis

But, I like to swap the concept around.

Any language that affects the way you think about programming is worth learning.

G. Wade Johnson’s Corollary

A few years ago, the articles about Rust began to get my attention. The discussion around it’s memory handling seemed interesting. I thought it might be worth learning just to see how this worked, even if I didn’t use the language for anything serious.

Interesting Parts in Rust

To be fair, many of the concepts in Rust have appeared in other languages already. In a few cases, Rust pushes the concept further than other languages. In other cases, Rust just implements the feature more cleanly than other examples I’ve seen.

Let’s start with the feature that first grabbed my attention: memory management.

Memory Management

One feature of Rust that I really like is no garbage collection. I’ve never liked garbage collection.

As an old C++ programmer, I was relatively comfortable with using smart pointers and careful design to avoid most memory problems. Over the years, C++ had begun experimenting with move semantics, but I hadn’t looked at state-of-the-art C++ in a while. Seeing that Rust had redefined assignments as moves seemed like the kind of interesting design decision that would change how I approach programming. That one decision opens up the possibility of easier tracking of the lifetimes of objects.

This lifetime issue and the rules around borrowing are the cause of most people’s complaints about Rust’s learning curve. But, for me, the interesting point was forcing me to completely re-examine some fundamental assumptions about the way code works. There are a number of reasons why assignment in most languages often ends up in shared references, rather than a move. Some of those reasons are much less important in an age of large amounts of memory.

Another interesting offshoot of this idea is the realization that we can calculate lifetimes of objects at compile-time. It really wasn’t all that long ago that the compiler barely had enough resources to do the translation language into machine code and handle basic peep-hole optimizations. Those optimizations have gotten better over time, but this kind of analysis is very different.

Type System

Rust’s type system is interesting. It’s sort of object oriented, but without any standard inheritance. That doesn’t seem like much of a downside to me. I know some cases where inheritance can improve a design, but it’s misused so much that I’m okay with removing it. I haven’t worked much with supertraits, which may cover that functionality.

Rust uses Traits to compose interfaces into a single type. Although I’ve seen traits or roles in other languages, Rust really pushes the concept to 11. Many Rust traits are really granular, maybe containing as few as one method. Many interesting structs may implement quite a few traits to define functionality needed for the struct.

There are a number of traits that you might almost think of as required for any type, such as Display, Debug, Eq, and others. Rust takes the C++ principle of not paying for features you don’t use extremely seriously. So none of these traits are assumed to be supported by a type. One genius decision though is the ability to supply obvious implementations of traits through the #[derive] attribute. Not all trait implementations are derivable, but the ones that derive supports are common enough that not having these traits attached to all types is not really a hardship.

Rust also supports enum types. An enum defines a single type with multiple variants, with the variant containing optional associated data. In my most recent posts, I covered a couple of enums that Rust implements that have interesting ripple effects on code design: Result<T, E> and Option<T>. These two examples are good examples of features of the language changing the way you think about programming. Long ago, we did something similar with a combination of C unions and tag integers used to distinguish which data the variable represented. It was manual and fallible, but had a similar feel. Rust enum variants implements this concept correctly and safely.

Pattern Matching

Although not used as extensively as in some other languages, Rust’s pattern matching does offer a different way of handling some kinds of programming problems. Pattern matching is handled through the match keyword, destructuring let, and the if let construct. The first two are pretty normal for languages with pattern matching. The if let construct is basically a match that handles one variation and handles all non-matching variants as else (if you supply an else).

One of the more interesting implications of Rust’s pattern matching is requiring exhaustive matching. It is an error to match on a value without matching all of the possible values. For values that are numbers or strings, this normally forces a default case to lump together values that we don’t handle specially.

Where exhaustive matching really shines, though, is with enum types. If match an enum value and don’t handle every variant of the enum, the code will not compile. This allows you to change enum variations as needed without having to search for every spot that you might have used one. If you miss a variation, the compiler will tell you.

Rust does give an ability to define an enum as non-exhaustive, but you must do it explicitly.

Iterators

Iterators are also not unique to Rust. But, I appreciate the way they are incorporated into the language including supporting iterators on non-container objects in an intuitive manner. Rust supports treating Result<T, E> and Option<T> as iterable containers in an interesting way, as well as treating arrays and slices as iterable.

The Iterator class also support a number of methods that make iterators more useful. There are variations on map, find, filter, sum, max, min, count, fold, for_each, and more that you would expect. But, some of my favorites include chain (for combining two iterators), cloned (for cloning all of the elements iterated over), collect (for making a collection out of an iterator), and others.

Rust also provides useful tools for making new iterators: empty, once, and repeat. This solves the problem of creating simple data to feed to code taking an iterator. Combined with chain, you can easily insert an item in front of some data to iterate.

Generics

Rust’s type system supports generics in a pretty straight-forward way. Rust learned from early C++ implementations that failed to constrain generics in some way to make them easier to reason about. Rust does this by declaring traits as bounds on the functionality supplied by the generic type. Among other things, this allows the compiler to supply much better error messages (unlike some of the early C++ template error messages that were a bit inscrutable.)

Macros

Rust’s macro system is different than the macro systems I have dealt with in the past. It is very different than the text-replacement version I’m used to from the C-derived languages, the code executed as compile-time system from Forth, or the macro system in Lisp. Although it seems most similar to the last.

In honesty, the language supports at least two different macro systems and I’m still trying to reach a level of comfort with them.

Conclusion

Most of the features of Rust are not completely new. They have been in use in other languages. Rust tweaks these features and combines them in interesting ways to make a language that feels very different. This list also doesn’t cover every feature of Rust, but only the ones I found encouraged a different approach to programming.

Making many of these abstractions cost-free at run-time removes the thought of trying to avoid them in low-level code. The compiler and linter (clippy) does a great job of leading you in the direction of idiomatic Rust.

The overall result is a language (and tool chain) that changes the way you think about code, making it well worth learning.

Leave a Reply

Your email address will not be published. Required fields are marked *