Rust Book, Chapter 2: Programming a Guessing Game

Chapter Two introduces a wide range of Rust constructs to build something a little more substantial than Hello, World. In this post, we’ll try to map Rust’s approach to mutability, error handling, and side effects to val=/=var, Either, and IO. None of these mappings is perfect, because the two languages diverge in more than syntax, but much remains familiar.

New projects #

cargo new is much like sbt new, but lacks the templating power of giter8. It isn’t as necessary in Rust, because we’re not trying to embed our domain name in a package statement or directory structure, but expect to add your own project metadata, like authors and license.

let and let mut #

Scala has two orthogonal notions of mutability:

  • Reference: A var can be reassigned to a different reference, whereas a val can’t.
  • Data: An Array is mutable, and a List is immutable.
Immutable reference Mutable reference
Immutable data val x = List(1, 2) var y = List(1, 2)
Mutable data val z = Array(1, 2) var w = Array(1, 2)

In Typelevel Scala, we aspire to the top left quadrant, avoid the bottom right. Sometimes we put one degree of mutability in IO, but carefully wash our hands before returning to work.

In Rust, both kinds of mutability are determined by the mut keyword.

Immutable reference Mutable reference
Immutable data let x = vec![1, 2]
Mutable data let mut w = vec![1, 2]

Instead of separate mutable and immutable collections, the individual operations may take either an immutable or mutable reference to the collection. This lets us mutate the vector referenced by w (e.g., w[0] = 3) or reassign w to a new Vec (e.g., w = vec![3, 4]). Both of these are disallowed on x.

Associated functions #

Rust’s associated functions, like String::new(), serve a similar purpose to Scala’s companion objects.

Result #

A Rust Result is either Ok or Err. Either? Yeah, like Scala’s Either! Scala calls them Left and Right, and it’s only by convention (and the convenience of map and friends) that the okay type goes on the right and the error type goes on the left. Rust gives us better names, which is a good thing, because it flips the order of the parameters from what Scala developers are used to.

Rust’s judgmental names sound more like Scala’s Success and Failure from Try. But only a Throwable can go in Failure, whereas Rust allows anything in Err, just like Left.

Footguns #

Like Scala, Rust promises us safety and mostly delivers, but gives us a few functions to point at our feet and shoot off our toes. Rust’s result.expect(s) is similar to Scala’s option.get, but worse: Scala throws an exception that might be recovered, whereas Rust panics before a dramatic, error-coded exit. Like Scala, linters can help. Like Scala, such methods are often less verbose if you think you’re smarter than the type system. Like Scala, you’re usually not.

Lockfiles #

We won’t go deep into crates vs. jars now, except to emphasize that a Cargo.toml file can resolve to different versions of dependencies over time. If you don’t check in your Cargo.lock today, you may be saying “worked on my machine” to a frustrated colleague in the future.

We aren’t in FP anymore, Alice #

This chapter is printing to the console, reading from the console, generating random numbers, and nary an IO in sight. This is not different from standard Scala, but is jarring coming from Typelevel Scala.

We find a lot in Rust that’s familiar: its enums are like Scala’s sealed traits. We already discussed Result and Either. Its pattern matching brings a new syntax but most of the same benefits and safety. But to be successful in this journey, we need to accept that mutability is more acceptable, and that we’ll rely on different safety nets like the borrow checker to keep us safe. You’ll miss some things and appreciate other things and that’s okay because they’re different languages, even if a lot of us enjoy both.

Shadowing #

In Scala, it’s frowned on to shadow an existing variable in a tighter scope, and forbidden in the same scope. It’s socially acceptable in Rust.

CC-BY-SA-4.0

let mut guess = String::new();

io::stdin()
    .read_line(&mut guess)
    .expect("Failed to read line");

let guess: u32 = guess.trim().parse().expect("Please type a number!");

This can get confusing in a large function. In a small function, it ties together the transformation from an unwanted, mutable String to the desired, immutable Int without any namespace pollution. Use with care.