Other Reasons Why Rust Is My Favorite Programming Language

Jeffrey Rennie
ITNEXT
Published in
10 min readJun 24, 2021

--

There’s a lot of hype surrounding Rust now. Some features dominate the headlines:

  1. Rust is as fast as C or C++.
  2. Rust is memory safe.

Rust programmers also enjoy great documentation and a great package manager, which no doubt have contributed to Rust’s growing popularity.

There are a few more features of the Rust language that I thoroughly enjoy, but I haven’t seen discussed much. I’ve been coding in Rust for about a year, and in that time it has become my favorite language among the dozens I know.

Here are three less-talked-about language features that make programming in Rust a joy:

  1. There’s no null.
  2. Powerful macros.
  3. Precise control over mutability.

There’s also a bonus feature I’m saving for the end, that only makes sense after explaining the other features.

The Rust cheat sheet quickly explains some of Rust’s unique syntax in the code samples that follow.

Let’s dive deeper into each feature.

There’s no null

Null is probably the single greatest mistake in programming language design. Tony Hoare called it his “billion-dollar mistake.”

Have you worked on a software project that never threw a NullPointerException or dereferenced null and crashed? If your project was written in Rust, Swift, Elm, or Haskell, then maybe you have. Otherwise, the chances are slim to null (pun intended).

Dereferencing null will bring a program to a screeching halt. To cope with the constant danger and avoid crashing their software, programmers write code like this:

This code is so dominated by null checks that it’s hard to understand what the function actually does. The poor programmer wrote more lines of code to cope with the possibility that an argument may be null than they wrote lines of code to solve the problem they wanted to solve. What’s more, programmers have read this null checking code so many times that they’ve trained themselves to ignore it. Did you see the bug I introduced in the code above?

Because Rust doesn’t have null, the sample code above could be written in Rust something like this:

The Rust code doesn’t check for nulls because neither argument can be null. Any attempt to pass null would be caught by the compiler and reported as a type mismatch. Now that I know Rust, I cringe when I see null checking in other languages.

But of course, null can be very useful. Sometimes it’s convenient for classes or structures to optionally hold values, and for functions to optionally return values. For that purpose, Rust provides the Option<T> type. An instance of Option<T> contains either None or Some(T) but never both.

Let’s consider a structure that represents a url like “http://www.google.com/”. Urls optionally contain a port number like this: “http://www.google.com:80/”. In Rust, we may represent a parsed Url with the following struct:

Imagine we want to write a method called incr_port() which increments the port number in a url; we’ll invoke the method like this url.incr_port();.

Naively, we might write the method like this:

The Rust compiler rejects incr_port() above because port is not a u16; it’s an Option<u16>. The compiler prevents what would have been a latent null pointer exception in other languages.

14 |      self.port += 1;
| ---------^^^^^
| |
| cannot use `+=` on type `std::option::Option<u16>`

To increment the port, the code must first confirm that port has some value.

With Option<T>, Rust provides optional values when we need them, without the possibility of crashing the program. More importantly, Rust saves the programmer from the chore of null checking every argument and object.

Powerful macros

Let’s continue with the Url struct above. I’ll add one line of code before the Url struct, and then discuss all the good things this one line does:

By adding this one line of code,

#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Hash, Default, Debug)]

I can now compare two Urls for equality like this:

url == url2

I can compare them for ordering too. The PartialOrd & Ord macros compare members of the struct in the order in which they appear in the struct.

url > url2

Which means I can put Urls into sorted containers:

let mut sorted_urls = BTreeSet::new();
sorted_urls.insert(url);

I can clone a Url to get an exact copy:

let url2 = url.clone();

I can insert Urls into hash containers:

let mut urls = HashSet::new();
urls.insert(url4);

I can create a default Url, kind of like invoking a parameterless constructor in other languages:

let url = Url::default();

And, I can print a pretty debug string for a Url:

println!(“{:?}”, url);

Output: Url { protocol: “http”, host_name: “www.google.com", port: Some(80), path: “” }

That’s a ton of functionality to pack into one line code! In other languages, there are ways to implement the same behavior: code generators, post processors, IDEs, etc. Some languages use introspection. However, I like Rust’s solution best because no additional tools are needed, there’s no additional code for me or my peers to review and maintain, and type errors will be caught at compile time.

These macros all behave like you expect. For example Ord, short for ordering or ordered, compares each member of the struct in order. This may trip people up while refactoring code, because changing the order of the members of the struct will change the sort order. To avoid that, I could implement std::cmp::PartialOrd and tailor it to my needs.

Third-party libraries can also take advantage of the #[derive()] macro. The serde library is Rust’s de facto standard for serializing/deserializing structs into/from more than a dozen formats. Let’s add some code to allow a Url to be serialized to and deserialized from json. After importing the serde library, I add two more attributes to the #[derive()] macro:

#[derive(…, Serialize, Deserialize)]

And now I can serialize and deserialize Urls from json.

let text = serde_json::to_string(&url)?;
let url2: Url = serde_json::from_str(&text)?;

Parse errors are immediately returned to the calling function via the ? operator.

The #[derive()] macro added a ton of functionality to my Url struct by adding only two symbols to my code! Had Url contained a type that could not be serialized to json, the compiler would have reported an error.

With great power comes great responsibility. If you maintained a C or C++ code base in the 1990s, then you probably saw people do terrible things with macros. The C/C++ preprocessor’s macros can replace any symbol with any text. People used that power to create code that looked nothing like C, and was very hard to understand and maintain. Here’s a real example from the era:

This code actually defines a method, and builds an if-else ladder in the body, but you’d never know unless you dissected the macros. Examining this code in the debugger is also confusing. Here’s what BEGIN_MESSAGE_MAP() looks like:

Unsurprisingly, there was a backlash against macros for creating this mess. The languages born from this experience intentionally lacked macros: namely Java and Go.

But I think those languages threw the baby out with the bath water. The issue wasn’t macros in general; the issue was the C/C++ preprocessor. After using dozens of libraries in Rust, I have yet to see anything as mysterious as BEGIN_MESSAGE_MAP(). In fact, I’ve only used macros from two libraries: serde and anyhow.

I see a few reasons why programmers have been more disciplined with macros in Rust than in other languages:

  1. Rust provides const functions, which can replace macros in many cases. For example, you could write a const sqrt() function to calculate the square root of a constant at compile time.
  2. With Rust macros, you can’t super-globally replace one symbol with any text you want. For example, this is impossible: #define PLUS +
  3. In Rust, macros must expand to complete expressions, statements, items, types, or patterns. They can’t expand to half a function. They can’t have unbalanced parenthesis, brackets or braces. The BEGIN_MESSAGE_MAP() macro above would be impossible in Rust because it has unbalanced braces.
  4. Defining a macro in Rust is much more complicated than with the C/C++ preprocessor. Learning how to write a macro in Rust is like learning a whole new programming language. You can’t just #define max(a, b) ((a) < (b) ? (b) : (a)). If something can be expressed any way besides a macro, Rust programmers like to choose the other way.

Precise Control over Mutability

Rust gives the programmer precise control over mutability. Mutable data can be modified. Immutable data cannot be modified.

Immutable data is great for everyone who has to read code, be they human or machine. It’s far easier to reason about immutable data, it’s easier for the compiler to optimize code that uses immutable data, and immutable data is always thread safe.

Consider this sample code that uses the Url sample from above and demonstrates why immutable data makes code easier to reason about.

Because url is passed to fetch() via an immutable reference, it’s impossible for fetch() to modify the contents of url¹. Therefore, it’s easy to predict the value of url through each iteration of the loop. I don’t have to inspect fetch()’s code or even look at its argument types to confirm that it doesn’t modify url, because I can see the argument is passed as an immutable reference: &url. To enable fetch() to modify url, I’d have to change the calling code to pass url as a mutable reference, like this: fetch(&mut url).

At the same time, I don’t have to make url immutable in the context of the whole loop to make it immutable during the call to fetch(). I still can still modify url by calling url.incr_port() within the loop.

Passing url as an immutable reference also provides guarantees to fetch(). It’s not only impossible for fetch() to modify url, it’s impossible for anything else to modify url while fetch() has an immutable reference to url. No other thread can modify url while fetch() is executing. That makes writing fetch() easier.

I say that Rust gives the programmer precise control over mutability, because Rust doesn’t entangle mutability with other concepts. The programmer can choose for data to be mutable or immutable regardless of whether the data is allocated on the stack or on the heap, regardless of whether it’s passed by reference or by value, and regardless of whether it’s a struct or a primitive type like an integer.

Without precise control over mutability like Rust, other languages have introduced a variety of patterns and language features to provide some form of immutability. Some examples are the builder pattern, read-only interfaces, and multiple aggregate types each with their own mutability rules like class vs struct vs record. Using these other solutions requires evaluating trade offs or writing more code than would be required in Rust.

Bonus Feature: Rust is Expressive

Rust has been described as an expressive language; expressive is a very abstract term. What is expressiveness and why is it useful?

In an expressive language, it’s possible to introduce new ideas that the language designers never dreamed of without changing the syntax of the language and without boilerplate code. The fact that many concepts were incorporated purely via a library rather than syntax is evidence that Rust is expressive.

For example, the Option<T> type is not built into the language; it’s just a standard library. Other languages required dedicated syntax to encode optional values, but with Rust, the existing language features allowed programmers to express optional values efficiently with Option<T>; no changes to the language were necessary.

Another example of Rust implementing a concept with a standard library rather than syntax is Rust’s From<T> and Into<T> traits, which allow programmers to write custom conversions from one type to another. Other languages added operator syntax to give programmers the ability to define custom type conversions. With Rust, the existing language features were sufficient. From<T> and Into<T> are ordinary traits.

This all sounds great for language designers, but how could it be practical for the rest of us?

Consider this function:

The io::Result<()> return type can contain exactly one of the following: an Error value, or the empty value () signifying success.

Imagine implementing this function. To make sure we always reset the file pointer to its original position, it sure would be convenient to have something like try {} finally {} found in other programming languages. Then, we could simply call f.seek(original_offset) in the finally block. But Rust doesn’t have exceptions, and it doesn’t have try {} finally {}.

To cope with the absence of try {} finally {}, I wrote a macro that behaves the same way. First, some disclaimers:

  1. This is the first macro I’ve ever written, so it surely can be improved.
  2. This is the first macro I’ve ever written, because I haven’t had the need to write a macro.
  3. This macro is for demonstration purposes only. When I actually had the same need in production code many months ago, I wrote plain Rust code.
  4. Because Rust has a built-in macro called try!(), I had to name my macro something different, so I named it tryf!().

Enough disclaimers. Here’s what the function implementation looks like with my tryf!() macro:

The syntax is a bit different, but if you’re familiar with languages with exceptions, I hope it’s obvious what this code does. That’s the beauty of an expressive language. I can take ideas that are foreign to the language itself and express them efficiently in Rust.

Because Rust is expressive, I’m confident that I’ll be able to efficiently express a great variety of my own ideas in code, with mistakes caught at compile time, and without boilerplate code.

Conclusion

Of course, I also like Rust for the same reasons as so many others: similar speed to C/C++, memory safe, great docs, great package manager, runs in the browser via WebAssembly, etc. But I thought Rust also had some hidden gems, so I wrote this post.

If you’re considering adopting Rust for work or hobby, I hope this post has given you some useful insights to help you make a more informed decision. To begin learning Rust, I recommend starting with the book Rust by Example.

Acknowledgements

Thank you to the following people who no doubt have different opinions about their favorite languages, but generously reviewed drafts of this post.

Foot Notes

[1] Sometimes, you may need to add a reference count or the like to an otherwise immutable struct. For situations like these, Rust provides Cell, Rc, and more.

--

--