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
varcan be reassigned to a different reference, whereas avalcan’t. - Data: An
Arrayis mutable, and aListis 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.