I've been working with rust for a little while
(read: a year-ish). Generally, I'm a proponent of the language and typically
enjoy working with it in both my career and on personal side projects. That
said, I'm also a slow learner. I seem unable to grok rust's if let
syntax.
Whenever the opportunity to use it presents itself I just continue doing the
same verbose match
syntax. It's a rather silly predicament. Whenever it's
pointed out that I could refactor to utilize if let
then I invariably realize
that I indeed could. Why it takes someone pointing it out to me at this point
in my life is unknown.
At the recommendation of my friend sleepysticks I've decided to take this particular gap a bit more seriously and write about it. Hopefully, it'll solidify if I work through my thoughts, right?
What is it? Concise control flow
Summarily, if let
is syntactic sugar that combines if
and let
into a
concise package.
The
if let
syntax lets you combineif
andlet
into a less verbose way to handle values that match one pattern while ignoring the rest.
Thanks Rust Programming Book.
I have been told repeatedly that it's useful when we only care about one match
arm. That's totally straight forward, right? It's a short-hand for match
where we only care about a single pattern.
Example patterns
I was thinking, perhaps, I'm missing these patterns because I end up seeing slight variations. Still where we only care about a single match arm but variations nonetheless. So, let's go through some examples to get this concept to stick.
For the sake of being thorough I'm going to write verbosely. As a matter of preference, I like it when things are extensively written out so I can reason my way through it. I find it especially helpful when I'm either trying to memorize something or if I'm learning a new concept. All that said - you should be able to copy and paste the following snippets into somewhere like the Rust Playground and they should work - given you aren't from the future and the language hasn't drastically changed syntax.
Option
Here, we only care about an Options
if it's something:
/// Demonstrate `match` syntax with an `Option`
fn main() {
let x: i32 = 3;
match foo(x) {
Some(a) => println!("{}", a),
_ => ()
}
}
/// Wrap some i32 in `Some()`
fn foo(x: i32) -> Option<i32> {
Some(x)
}
Which can be refactored into if let
syntax like so:
/// Demonstrate `if let` syntax with an `Option`
fn main() {
let x: i32 = 3;
if let Some(x) = foo(x) {
println!("{}", x);
}
}
/// Wrap some i32 in `Some()`
fn foo(x: i32) -> Option<i32> {
Some(x)
}
OK Result
Similarly, we can apply this shortcut to Results
:
/// Demonstrate `match` syntax with an `Ok()`
fn main() {
let x: i32 = 3;
match foo(x) {
Ok(a) => println!("{}", a),
_ => ()
}
}
/// Wrap some i32 in `Ok()`
fn foo(x: i32) -> Result<i32, &'static str> {
Ok(x)
}
Refactored with if let
:
/// Demonstrate `if let` syntax with an `Ok()`
fn main() {
let x: i32 = 3;
if let Ok(x) = foo(x) {
println!("{}", x);
}
}
/// Wrap some i32 in `Ok()`
fn foo(x: i32) -> Result<i32, &'static str> {
Ok(x)
}
Err Result
Maybe there are times that we just want to check if something is an Err()
and do something special if it is. We can use the pattern here too!
/// Demonstrate `match` syntax with an `Err()`
fn main() {
let x: i32 = 3;
match foo(x) {
Err(e) => println!("{}", e),
_ => ()
}
}
/// Return an `Err()`
fn foo(x: i32) -> Result<i32, &'static str> {
Err("That didn't work by design")
}
Refactored into if let
:
/// Demonstrate `if let` syntax with an `Err()`
fn main() {
let x: i32 = 3;
if let Err(x) = foo(x) {
println!("{}", x);
}
}
/// Return an `Err()`
fn foo(x: i32) -> Result<i32, &'static str> {
Err("That didn't work by design")
}
The trivial examples make sense. They're also incredibly similar to one another but I think they get the gist across.
Thoughts
I've been staring at these if let
examples for a while trying to deduce what
my hang up is. I think that I'm not convinced the point of the expression
makes sense as a shortcut for a one-arm match
. By that I mean that the way
I'm reading it out loud isn't an easy translation to what's happening for me.
The way I read it at first glance makes me think it's letting something be a
Result<T>
or an Option<T>
that still needs an unwrap()
- which isn't true
considering how we consume T
inside of the if let {}
.
if let Ok(x) = foo(x) { // we can just use `x` in here }
I can reason that, after a pinch of mental gymnastics on my part, what's actually happening here is more like:
If foo(x)
returns an Ok(x)
then let x
be the unwrapped contents of
that Result
. Which, I guess isn't much different than a match
arm in that
it's unwrapping things for you so you can just use whatever it is after
the =>
.
Fine. That makes sense.