First Rust Program Pain (So you can avoid it…)
Like many programmers I like to try out new languages. After lunch with Alex Crichton, one of the Rust contributors, I started writing my favorite program in Rust. Rust is a "safe" systems language that introduces concepts of data ownership and mutability to semantically prevent whole categories of problems.
It's primarily developed at Mozilla Research in service of a next generation rendering engine, and while I presume that the name is a poke in the eye of Google's Chrome, no one was brave enough to confirm that lest their next Uber ride reroute them to Bagram.
My standard "hello world" is a anagrammer / Scrabble cheater. Why? In most languages you can get it done in a few dozen lines of code, and it uses a variety of important language and library features: lists, maps, file IO, console IO, strings, sorting, etc.
Rust is great, interesting in the way that I found objected-oriented or functional programming interesting when I first learned about them. It's notions of data ownership, borrowing, and mutability I think lead to some of the same aha moments as closures for example. I found Rust to be quirky enough though that I thought I might be able to save others the pain of their first program, advancing them to the glorious, safe efficiency of their second by relating my experience.
So with the help of Stack Overflow I wrote the first chunk:
So far so good? Well I ran it and it didn't seem to be terminating...
Okay -- first lesson: String::clear(). As the documentation clearly states, BufReader::read_line() appends to an existing string; my own expectations and preconceptions are beside the point.
It turns out that BufReader::read_line() indeed is_ok() even at EOF. Again, documented but--to me--counter-intuitive. And it turns out that this is a somewhat divisive topic. No matter; how about something else? Well it works, but the ever persnickety rustc finds 'while true' too blue-collar of a construct:
Trying to embrace the fastidious methodology (while ever temped to unsafe-and-let-execution-be-the-judge) I gave up on read_line() and its controversial EOF and error semantics to try out BufReader::lines():
Okay; that was apparently very wrong. The BufReader::lines() iterator gives us Result<String>s which we need to unwrap(). No problem.
Fine, rustc, you're the boss. Now it's simpler and it's cranking:
Now let's build up our map. We'll create a map from the sorted characters to the list of anagrams. For that we'll use matching, another handy construct.
What could be simpler? I love this language! But not so fast...
This is where in C I'd start casting away const. Not an option here. Okay, but I remember these notions of ownership, borrowing, and mutability as concepts early in the Rust overview. At the time it seemed like one of those explanations of git that sounds like more of a functional analysis of cryptocurrency. But perhaps there were some important nuggets in there... Mutability, check! The Hashmap::get() yielded an immutable borrow that would exist for as long as its return value was in scope. Easily solved by changing it to a get_mut():
Wrong again. Moving me right down the Kubler-Ross model from anger into bargaining. You're saying that I can't mutate it because I can already mutate it? What do I have, rustc, that you want? How about if I pull the insert() out of the context of that get_mut()?
Inelegant, yes, but Rust was billed as safe-C, not elegant-C, right?
So by pushing the anagram into the list at line 30 we lost ownership, and even though that definitely didn't happen in the case of us reaching line 37, rustc isn't having it. Indeed there doesn't seem to be a way to both get an existing value and to insert a value in one lexical vicinity. At this point I felt like I was in some bureaucratic infinite loop, doomed to shuttle to and fro between windows at the DMV, always holding the wrong form. Any crazy person will immediately be given an mutable map, but asking for a mutable map immediately classifies you a sane. After walking away for day to contemplate, here's the compromise I came to:
And everyone was happy! But it turns out that there's an even Rustier way of doing this (thanks to Delphix intern, John Ericson) with a very specific API:
This is starting to look at lot less like safe C and a lot more like the stacking magic of C++. No matter; I'm just trying to cheat at Scrabble, not debate philosophy. Now that I've got my map built, let's prompt the user and do the lookup. We'll put the string sorting logic into a function:
This was wrong because we need to pass s as a reference or else its borrowed and destroyed; this needs to happen both in the function signature and call site.
As an aside I'd note how goofy I think it is that the absence of a semi-colon denotes function return. And that using an explicit return is sneered at as "un-idiomatic". I've been told that this choice enables deeply elegant constructs with closures and that I'm simply behind the times. Fair enough. Now we'll read the user-input:
Okay! Too cute! Got it. Here's the final program with some clean up here and there:
Rust is not Python. I knew that Rust wasn't Python... or Java, or Perl, etc. But it still took me a while to remember and embrace that. You have to think about memory management even when you get to do less of it explicitly.
For programs with messy notions of data ownership I can see Rust making for significantly cleaner code, easier to understand, and more approachable to new engineers. The concepts of ownership, borrowing, and mutability aren't "like" anything. It took the mistakes of that first program to teach me that. Hopefully you can skip straight to your second Rust program.
Before I posted this I received some suggestions from my colleagues at Delphix about how to improve the final code. I resolved to focus on the process--the journey if you will--rather than the result. That said I now realize that I was myself a victim of learning from some poor examples (from stack overflow in particular).
There's nothing more durable than poor but serviceable examples; we've all seen inefficient copy/pasta littered throughout a code base. So with the help again from John Ericson and the Twitterverse at large here's my final version as a github gist (if I was going to do it over again I'd stick each revision in github for easier navigation). Happy copying!