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.
Early versions of Cats had `Xor`. Not only was my shitpost that we should call them `Sinister` and `Dexter` denied, but the type was removed altogether.
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.