I looked for a joke about null pointers in Rust, but there was None.
Rust has no null. Instead, when a value might be absent, the type makes
that explicit using Option<T>:
enum Option<T> {
Some(T),
None,
}
The compiler will not let you accidentally use a None as if it were a
real value. Every time you have an Option, you have to deal with both
cases. That's the whole point.
There are two main ways to unwrap an option. Pattern matching is the fundamental tool:
match find_user(id) {
Some(name) => println!("found {name}"),
None => println!("no such user"),
}
But for common cases there are shorter combinators:
let port = settings.port.unwrap_or(8080); // value or fallback
let upper = name.map(|s| s.to_uppercase()); // transform if Some
let len = maybe_str.map_or(0, |s| s.len()); // transform-or-default
|x| ... (closures)Those |s| s.to_uppercase() and |s| s.len() bits are closures:
anonymous functions you can pass as arguments. The pipes hold the
parameters; everything after them is the body:
let add_one = |x| x + 1;
add_one(2); // 3
If the body needs multiple statements, wrap it in braces:
let greet = |name: &str| {
let trimmed = name.trim();
format!("hello, {trimmed}")
};
Closures show up properly in chapter 15. For this chapter, just read
|s| s.len() as "a tiny one-shot function that takes s and returns
s.len()."
A useful one to know: if let lets you handle just the Some case
without writing a full match:
if let Some(user) = find_user(id) {
println!("welcome, {user}");
}
Many standard-library methods return Option. .first(), .last(),
.next() on iterators, .get() on slices and maps, .find(...) on
iterators. You'll meet Option everywhere.
The simplest Option pattern: you're handed one, and you either use
what's inside or fall back to a default. match works, and Option
also has a few helper methods that are shorter.
Useful from the standard library
Option::unwrap_orreturns the inner value whenSome, the argument whenNone. The reach-for-this-first method.Option::unwrap_or_defaultis the same idea but usesT::default()(which foru32is0).- A
matchalways works too:match setting { Some(v) => v, None => default }. Pick whichever reads better.
Same idea as before, but now the fallback isn't the value itself. You
need to call .len() on the inner string first. A match makes both
branches explicit; iterator-style methods on Option are tidier once
you spot them.
Useful from the standard library
Option::mapapplies a function inside theSomeand leavesNonealone. Somaybe.map(|s| s.len())produces anOption<usize>.Option::map_orcollapses both steps into one call: a default forNoneand a closure forSome. Reads asmaybe.map_or(0, |s| s.len()).- The chapter intro explains the
|s| ...closure syntax. For now read it as a tiny one-shot function fromsto its body.
Now you have to produce an Option, not consume one. Asking a
string for its first character is the canonical example: if the
string is empty, there is no first character, and returning some
"default" char would be a lie. Option<char> is the honest type.
You could pattern-match by hand, but the standard library has
already done the work for you: text.chars() returns an iterator,
and every iterator's .next() already hands you Option<Item>.
Compose the two.
Useful from the standard library
str::charsreturns an iterator over thechars of the string.Iterator::nextpulls one item off the iterator and returns it asOption<Item>. For the first character, that'sOption<char>and exactly the return type.
The trickiest of the four: produce an Option by searching. The
iterator chapter is still ahead, but slice::iter() plus a search
combinator already gets you most of the way; the matched tuple still
needs to be reduced down to just the username.
Type walk-through (this is the puzzle):
users.iter() yields &(u32, String).find(|(uid, _)| *uid == id) yields Option<&(u32, String)>.map(|(_, name)| name.as_str()) yields Option<&str> (the return type)name.as_str() turns the &String we destructured out of the tuple
into the &str the signature wants.
Useful from the standard library
<[T]>::iteryields shared references to the slice's items, one at a time.Iterator::findtakes a predicate closure and returns the first matching item as anOption.Option::maptransforms the inner value when present. Here it pulls the username out of the tuple and converts it to&str.String::as_stris the explicit "borrow thisStringas&str" call.
You consumed Options with fallbacks and combinators, produced new
ones from string and slice operations, and chained find and map
to turn a search into the exact return type the signature asked for.
What we learned
Option<T>is Rust's stand-in for "value or absence". The compiler forces both cases to be handled, which is why there's nonull.matchis the always-works tool. For common patterns, reach forunwrap_or,map, andmap_orto keep call sites short.if let Some(x) = ...is the lighter alternative tomatchwhen you only care about theSomebranch.Option::mapmirrors the iterator method of the same name: it transforms the inside if present, leavesNonealone.- Many standard-library operations already return
Option:iter().next(),slice.first(),Vec::pop,HashMap::get,iter().find(...). Once you spot them, "I have an option" is often the answer to "how do I express absence here?".unwrapandexpectextract the value but panic onNone. Use them in tests or when you've already ruled outNone; otherwise prefer the safer combinators.- The
|x| ...syntax is a closure: a tiny anonymous function. It shows up everywhere withOptionand iterators. Chapter 15 covers closures in their own right.