Learning Rust

thumbnail

Six years ago, during my short stint at The Weather Company, I was on the lookout for a programming language that would let me build software with no dependency on the runtime. There were two obvious candidates: Go and Rust. For many reasons, Rust had me a little bit anxious. The borrow checker, which I just could not fully understand back in the day, was the main sticking point. There were others like the concurrency story, which wasn’t very clear, with multiple implementations competing for the crown.

Go is an opinionated programming language. Some people have a problem with that, and I found that refreshing. It took a few hours to get comfortable with the syntax, but within a day, I had my first program completed. What a breath of fresh air that was.

  • The compiler was fast.
  • The resulting binaries were small compared to your regular JVM-based language build artifacts or Erlang distributions.
  • There was no runtime! Dropping them into a scratch Docker image resulted in very small, portable, quick and easy to deploy programs.
  • One binary per target architecture, and I could cross-compile to different architectures right on my computer.
  • Working with dependencies wasn’t as easy as it is today with modules, but even something like dep was easy enough. I very much like how dependencies come simply by telling the toolchain to import a library from the URL.
  • The ecosystem was very rich. There were so many libraries to choose from and learn from.

Some things took longer to learn. Mastering channels and figuring out how to write concurrent code required some practice. I was never bothered with nil. I have worked with nulls for long enough to acknowledge their existence and deal with them. I also don’t have a problem with how error values are handled in Go. They’re explicit, and I like explicit code. Admittedly, sometimes it is a bit tedious having to handle them every second or third line, but that’s a small price to pay for all other benefits.

There were some things I was missing. Before diving into Go, I worked a lot with Erlang and Scala so not having pattern matching, higher-kinded types, currying, and sometimes generics, was often limiting.

Regardless, I have written so much Go in the last few years, and I enjoyed every single line of code.

Go gets things done.

§rust

Last year I worked on the multi-tenant YugabyteDB implementation. That project took me out of my comfort zone in a positive way.

As part of that solution, I’ve built a Postgres extension in C, and I also contributed a couple of features to YugabyteDB, which required working with C++. I did not work with those two technologies before at all. But the amazing thing happened: I experienced the exact problem that Rust was claiming to be solving. GCC was barfing at me about variable lifetimes. Like I said, my C and C++ were pretty clumsy. But it gave me a chance to to understand lifetimes first hand.

And recently, Jatin asked me the following question:

What’s your koolaid now? Go? Rust?

Of course, I answered that I write a lot of Go. However, that question had some interesting side effects.

Rust is very popular. The ecosystem is growing, there is so much interesting code written in it nowadays, and I’ve been pondering the idea of giving it another try anyway. And it finally happened, it was fun!

The tooling is really good. I used rustup to install Rust and the toolchain. My editor of choice is Visual Studio Code. Instead of the Rust extension, I decided to use rust-analyzer. This extension provides inline type annotations, code hints, syntax highlighting, go to definition, and more.

This time, rather than going through The Rust Book[1], I jumped right in with a goal of building a complete program.

The best method to evaluate a programming language is to apply it to the same class of problems one is already familiar with. As I have written many command line tools and plenty of networking code with TLS support, I went for a command line TCP echo server and client with TLS support in Rust. That felt challenging enough:

  • Command line with flags and arguments.
  • TLS with custom roots, certificates and private keys.
  • A concurrent TCP server.
  • A TCP client.

Certainly, Rust has gotten easier to use than what I used to remember from years ago. Certainly, the right tooling makes it easier to work with. Borrow checker, mutability, and lifetimes: while writing my first program, I haven’t had a need to think much about those. That was the biggest surprise.

Some additional thoughts:

  • Building command line applications with clap[2] crate is very easy.
  • Rust’s pattern matching is very refreshing. It’s one of those things I’ve been unconsciously missing in Go. Unlike Scala, Rust’s pattern matching is exhaustive.
  • Immutability by default. I enjoyed Erlang a lot. In Erlang, everything is immutable. I like this property because it makes is very easy to follow when state changes in the program.
  • The std::result::Result<T, E> type seems to be Rust’s composability workhorse.
  • The ? operator is magical. Given function’s std::result::Result<T, E> return type, if the code inside of the function results in an Error, the function returns early with an error. Otherwise, the variable (do we even have a word describing an immutable variable?) in the let variable = expression?; takes the value of T and the function execution continues uninterrupted. A fancy try. The code looks as clean as Scala with its map, flat map, flatten, and whatnot, but it handles errors magically.
  • I was expecting that the std::option::Option<T> type would be more common in my code, surprisingly that’s not the case. std::result::Result<T, E> works better.
  • Rust has type aliases.
  • I was caught out by dependency features in Cargo. It’s worth reading this article[3] before starting with Rust.
  • Macros will be a different game, but they are very powerful.
  • Inline type hints provided by rust-analyzer can be difficult to work with because they don’t show type names with full namespaces. I often see something returns an Error, but what exactly? std::io::Error? rustls::Error? tokio_rustls::webpki::Error?
  • Cargo is very nice. It was six years ago, no surprise here.
  • There are many ways of doing concurrent code in Rust. I went with Tokio[4], and I don’t think I’ll be looking for a different approach.
  • Personally, I find working with Rust’s documentation pretty difficult, the type syntax with lifetime annotations is noisy, but I’m sure it will get better over time. I attribute this to my lack of experience rather than an issue with the documentation itself.
  • Rust documentation comments are executable[5]. I haven’t explored them yet but they’ll be definitely very useful.

§but why?

Is there anything wrong with Go? No, there’s nothing wrong with it. I don’t see myself leaving Go anytime soon. Go has some very cool interfaces for working with HTTP, TLS, and SSH. Ultimately Go gets things done, indeed.

I have looked into Rust simply out of curiosity. It’s been a couple of evenings so far, 10 hours in total, maybe? But I enjoyed this brief exposure, and now I’m even more curious. It is good to be able to read Rust, and there are some very interesting developments in the WebAssembly area that I am looking forward to exploring.