I tried Rust
...and now I hate all other programming languages.
It’s December 2022, let’s try Rust 🦀
As you can tell by previous posts on this blog, I used to be quite a fan of Go; I use it at work often, and some features about it are legitimately great: Package management, “static duck typing” (structural typing), providing interfaces while stepping away from inheritance, all quite nice (and present in Rust). I wasn’t too unhappy with the repetitive error handling, generics are finally coming into play, and nothing I write is anywhere near performance-critical enough for me to care about GC overhead (though I did glance firmly at the binary size once in a while). But come December, as I decided to give Advent of Code a go this year, I figured I’d try to use it to learn a new language: Rust.
Now, Rust has been steadily gaining popularity for a while, but two recent events caused me to pay attention: In September, a CTO from Microsoft gave Rust a significant endorsement. In that same month, Linus Torvalds effectively announced that Rust was coming to the Linux kernel. When those two agree on something, I figured, it’s probably worth paying attention.
To my delight, someone else — fasterthanlime — was doing Advent of Code in Rust. In fact, he was doing a day-by-day “let’s learn rust while solving Advent of Code” series. Part 1 includes everything you need to get started, tooling and all, and a delightfully unusual introduction to file I/O which I won’t spoil.
Having spent some time with Rust, I now see more and more faults with other programming languages. Others have written many words about this; fasterthanlime has a couple of very detailed posts in this direction; the folks at Discord wrote a great post about switching from go to rust to eliminate GC latency. But I’d like to talk about something far, far simpler.
Let’s talk about null checks.
Things that may or may not be there
My initial sense of Rust is that it involves a lot of fighting with the compiler… and the compiler being right. Getting code to build is much more difficult than I’m used to, but when it builds — it works. Not always, but with a much higher likelihood than I’ve seen elsewhere. To explain this phenomenon, let’s take a look at cases when data is allowed to be absent.
It is often useful, in code, to deal with something that may or may not be
present. I’ve recently had the unpleasant experience of dealing with
soccer1 for work2; I still don’t quite get it, so this
example might not make any sense, but bear with me: Let’s imagine that a soccer
Team has several
players (each of which is a
Person with a
age), and may or may not have a
coach, who is also a
Person. In JSON, that
would look like this:
Suppose you write some code to handle such a
Team, and, say, return whether or
not any of the players are older than the coach.
In Go, you’d probably end up doing something like this:
At some point you will encounter a team without a coach, and your code will panic and exit with an error. It won’t even be a useful error message — it’ll be something like this (but probably with many more goroutines).
The issue is that the null (well,
nil) pointer is used as a way to indicate
“something that is not there”, and Go — just like C — uses pointers both to
indicate “we’re dealing with pointing at memory addresses” and to indicate
“we’re dealing with something that may be absent”.
Worse yet, because most teams do have coaches, this will be a rare case. It’ll likely be shuffled off into the back of the bug queue as a “rare crash”, waiting to jump on you when somehow a coach-free team makes it to the world cup finals.
Although rust does support pointers (null and otherwise), those are usually
unsafe code. In day-to-day rust, indicating that a
value might be absent is done using
std::Option. If you were
recreating the same naive approach as Go, you’d end up writing something like
The error you’d get for forgetting to check whether
is actually a bit better:
However, there’s an extremely handy smoking gun here —
unwrap itself. That’s
not a function that usually gets used in production3 code. A
reviewer or linter should be able to catch it. The function should actually be
written like this:
Importantly, the type of
definitely_a_coach is not
Option<Person> — it’s
Person. That is, when using
match (which is fairly standard), the guarantee
that “you made sure the thing is actually there” happens at compile time.
None case is a compilation error.
In fact, there’s an equivalent way to write this, shorter, but providing the same safety guarantees:
Importantly, the syntax that might panic (
unwrap) is quite different, easy
to pick out, and does not have to be used at all. In contrast, in other
languages, like Go, we don’t get the opportunity to notice that it’s happening.
coach pointer gets dereferenced using the same syntax, whether or not it’s
guaranteed to not be
This seems to be equivalent to the Haskell
Maybe type. If I were smart enough
to code in Haskell, I’d be sure. One of the nice things about Rust is that it
allows writing code in imperative style without understanding monads.
Java 8 introduced
java.util.Optional, which does the same
thing as Rust’s
Option. However, the safety guarantees are more limited:
You can check
get(), but this is no better than checking if a standard reference would be
null(that is — nothing makes sure that you did so, and if nothing is there —
get()throws an exception).NoteApparently some external inspectors do check for this, e.g. https://rules.sonarsource.com/java/RSPEC-3655
You can use
orElse(defaultValue), which makes sense in some cases, but not always (what if it’s a temperature-in-celsius that might be absent? You can’t use 0° as a default value).
You can use various other methods like
map, but that requires callback-style programming (which I don’t think is the norm for Java).
At the end of the day, Java’s legacy is probably a limiting factor here — your
code likely needs to interoperate with a pile of code that simply uses
the traditional way for “thing that is not there”.
Finally, researching for this post showed at least one guide claiming the
following as Misuse of
- Passing an
Optionalparameter to a method
- Having an
Optionalfield (also discussed here), exactly as we’re doing here.
…so I guess you’re stuck null-checking for those cases.
C++17 adds std::optional. I haven’t tried it out, but judging
from a quick read, it appears to be more robust than
Java’s, but still far less safe than Rust’s: You’re still checking
and risking an exception when calling
value() (…does your codebase even
allow for exceptions?), or using
value_or if a
sentinel value is acceptable.
Why does this matter?
Go is often regarded as a memory-safe4 language. And that’s technically correct in this case — if you get a null dereference, your code will simply crash, as opposed to some crazy Undefined Behavior. Presumably your production setup is resilient to crashes, and you’ll catch these crashes in pre-production anyway.
…except, it’ll take you a while to do that. And the crash will seem quite esoteric, and might not even happen in pre-production (does your test data contain teams without coaches?)… and, once again, if a coach-free team suddenly plays a very popular match, are you really set up to deal with such consistent crashes?
It’s possible to build automatic tooling for detecting these cases, and people far smarter than myself are already doing so. Unfortunately, applying them to legacy code is an even harder. I’ve seen such a “you did not check for null” static analyzer completely miss a case quite similar to the above; and while we did catch it in pre-production, a lot of people wasted a lot of needless time on it.
This is also only one (relatively-simple) example of what Rust does about
safety. A more elaborate example is mutexes: A rust mutex “holds”
the protected data, requiring you to
lock() it to even access the data. This
means that the type-system guarantees that the mutex protects whatever it’s
meant to protect. In Go, however, the protected value just wears the mutex as a
hat — so the compiler has no clue. (There’s at least one person
porting this idea into C++)
So examine your programming language; see what safety guarantees you’d like it to have (try to use the ones it already does!); and perhaps look at Rust for a bit of inspiration.
Short for — did you know? — Association Football. I live in Ireland, which plays multiple kinds of football, so I find “soccer” to be the more specific term. ↩︎
I really don’t like watching any kind of sportsball, but there was a fair bit of excitement around the recent FIFA World Cup, and my involvement extended to having to watch some of those matches. Live 🙄. In contrast, to relax in the evenings, I did AoC — so I effectively watch soccer for a living and code for fun. ↩︎