This is the first year I’m participating in Advent of Code at the suggestion of my colleague. After solving the first few days in Golang, I decided that it is a good opportunity to try out a new language.

I first heard of Rust through ripgrep. It was so fast and responsive to use as a replacement for grep that I got intrigued by the language it was written in. Later on, I became curious about its borrow checker, a unique mechanism to ensure memory safety.

This post is about my experience after programming in Rust to solve challenges in Advent of Code 2021.

PS: My initial post was about my experience after 1 week with the intention of a follow up post. But I got lazy and didn’t finish writing up the post. So it is now a combined post.

Developer Experience

One of the things I like about Go is how easy it is to get started. The instructions to install are simple, and things “just work” out of the box. Tools like gofmt, go get <package>, go run, go build, go test are all very intuitive to use. In contrast, I failed to self-learn C++ many years before Go, and it’s mostly because I couldn’t set up the compilation environment.

Onboarding

I was pleasantly surprized that Rust has a very comparable onboarding experience like Go’s. Rust comes with similar tools and more! Its package manager cargo has more batteries included than Go’s. For example, it defines what a good package structure is like: new packages are created with cargo new [--lib] <name>.

Looking back to the day when I started learning go, my colleague back then told me Go is a simple langauge and I would take 1 day to start getting productive. True enough, the “Tour of Go” was instructive to learn about the language features and I shipped my first production program the next day.

Rust has a similarly helpful onboarding material. I went through the book, which was quite detailed. On hindsight, I could have started with Rust by example which suits my learning style better. However, unlike Go, there’s quite a lot to cover, and the book isn’t something for one sitting. It took me several days to finish, and even then I’m not sure I have fully grasped the necessary concepts to write something production ready.

Documentation and Error Messages

The documentation for Rust felt more overwhelming than Go’s. It could be because of the way the information is laid out on the web site. Take for example the sort package. Go’s sort package feels inviting, with examples and all the functions layed out on one page. Rust’s equivalent is very reminiscent of c++ reference page and can be overwhelming. A beginner like myself could click around many links and still see no examples of how to use it.

One thing that I clearly noticed was that the compiler error messages from Rust are really helpful. It includes what the error is, and then sometimes a suggestion on how to fix it. For example, when I get an error, it gives me a link e.g. to explain it (no more searching for error code) with examples of how the rule is violated.

Editor

When I started with Rust, I used vscode. However, the lack of out-of-the-box tooling integration with vscode meant that I had to type the import statements manually. After 1 day, I switched to using intelliJ and its Rust plugin. That made my editor experience on par with that for Go. In this regard, I think IDE both both languages are equally good.

PS: vscode has excellent Rust support today.

Language Features

Rust has clearly incorporated several modern language features which makes programming in it ergonomic. Rust reminds me of Scala but without the burden of the JVM runtime. Here are the top few which caught my attention:

  • Type inference. This produces succinct code when the types are obvious.
  • Pattern matching and type checking to make sure it’s exhaustive. It feels so much safer to introduce new switch cases!
  • Generics. Coming from Go this is huge, it means less duplication and codegen.
  • Streaming API / Functional programming. It means code can be more declarative and reads more naturally.
  • Macros - E.g. deriving Enum strings, creating literal collections, printing and debugging.

I’ll just give some examples for the last two.

Streaming API

This makes it easier to write declarative code which makes code comprehension easier.

For example, iterating through the list can be done by turning it into an interator, and then mapping over the element. In Go, I’ll have to fall back to do it iteratively, and create a few more temporary variables that would be encapsulated away in the functional style.

Compare the code snippets for converting an array of integer to its string value:

In Go, this is the imperative code.

ints := []int{1,2,3,4,5}
var res []string
for _, i := range ints {
    res = append(res, strconv.Itoa(i))
}

In rust, the stream API can be used to express the job in a more declarative way. Map here has abstracted over the imperative version above.

let ints = vec![1,2,3,4,5];
let res = ints.iter().map(|x| x.to_string()).collect_vec();

Macros and Enums

Rust also has macros, which is effectively ergonomic code gen. For example, if I want to print the enum as a name, in Go, I’ll probably write something like:

//go:generate stringer -type Order
// And then I have to remember to run this whenever I add a new enum.
type Order enum {
    LessThan = iota
    Equal
    GreaterThan
}
// ...
func f() {
    x := LessThan.String()
}

In Rust, no explicit code gen is necessary.

#[derive(Debug)]
enum Order {
    LessThan,
    Equal,
    GreaterThan
}
// ...
fn f() {
    println!("{:?}",Order::Equal); // prints Equal
}

Productivity

I am really in love with shipping programs as single binaries because it simplifies deployment. Both Go and Rust does this and I see huge potential in becoming productive - writing code and deploying them - in Rust.

However, I’m still very early in my Rust journey to give any testimony.

Some Struggles

It’s not all sunshine and roses. I definitely struggled with the compiler a lot during this challenge:

  • Ownership is confusing. Writing generic functions with lifetime wasn’t immediately intuitive to me.
  • Casting between usize (for indexing) and i32 (for arithmetic) was annoying. E.g. doing index arithmetic to do wrapping around a grid.
  • Beginners like myself can end up writing awkward code: cost_grid.get(i).unwrap().get(j).unwrap().clone();
  • I struggled with the borrow checker where my algorithm tries to have two pointers into a list, where one of them is mutable. In theory my code is safe, but the borrow checker is unable to deduce that and prevents me from writing such code.

Conclusion

Learning another programming language has been a great to improve my programming skill. Using coding contests like Advent of Code is a practical way of learning to use a new language. I enjoyed using Rust and see myself doing the same next year!