Option<T
Option<T>
Level 2 — Control Flow & Data Structures An enum (
Some(T)/None) replacing null; forces explicit handling of absent values.
1. Prerequisites
- Enum —
Optionis just a standard Enum built into the Rust standard library! match— The safest way to handle both variants of anOption.if let/while let— The cleanest way to handle anOptionwhen you only care about theSomevariant.
2. Term Category
Rust-specific (the safety): Rust completely removes the concept of null from the language. Instead, it uses the Option<T> enum to safely model the concept of a value being absent or missing, entirely preventing "Null Pointer Exceptions".
3. Explanation
(1) Design Motivation — "Why did we design this?"
The inventor of null (Tony Hoare) famously calls it his "billion-dollar mistake." In languages like Java, C++, or JavaScript, null is a sneaky value that almost any object can secretly be. If you write code expecting a user's name, but you receive null and try to call .toUpperCase() on it, your entire program instantly crashes at runtime.
Rust bans null completely. Instead, Rust represents the possibility of absence using a built-in Enum called Option<T>. It has exactly two variants:
Some(value)— The data exists, and it's inside here.None— The data is missing (the safe equivalent of null).
Because it is an Enum, the Rust compiler forces you to handle the None case before it lets you touch the data inside Some. You literally cannot forget to check for "null" in Rust. The compiler will catch the mistake and refuse to build the program.
(2) Reality Metaphor
Imagine receiving a wrapped gift box.
In a language with null, you arrogantly assume there's a gift inside and reach in blindfolded. If the box happens to be empty (null), a booby trap snaps on your hand and you die (the program crashes).
In Rust, the Option type forces you to take off your blindfold and safely look inside the box first (using match). If the box is empty (None), you sigh and move on safely. If there is a gift inside (Some), you extract it and use it safely.
(3) Rust Code Examples
Short Snippet (The Definition)
You don't need to define Option yourself; it's already in the language. But if you did, it would look like this:
enum Option<T> {
None,
Some(T),
}
Because it's so common, Rust automatically imports the Some and None variants for you.
let present: Option<i32> = Some(5);
let absent: Option<i32> = None;
Fuller Example (Safe Extraction)
fn main() {
let middle_name = Some(String::from("Danger"));
// Attempting to do `middle_name.len()` right now will fail to compile!
// We must extract it first using pattern matching.
// Method 1: Using `match` (handles both cases)
match middle_name {
Some(name) => println!("Middle name is {} letters long.", name.len()),
None => println!("No middle name provided."),
}
// Method 2: Using `if let` (handles only the Some case)
let lucky_number = Some(7);
if let Some(num) = lucky_number {
println!("My lucky number is {}", num);
}
}
4. Common Mistakes & Pitfalls
Mistake 1: Misunderstanding Option T Scoping and Lifecycle Rules
The mistake: Assuming Option T instances remain valid beyond their declaring scope block or across asynchronous boundaries without explicit lifetime tracking.
Why it's wrong: Rust strictly enforces lexical scope boundaries and non-lexical lifetimes (NLL) at compile time. Accessing dropped values or failing to handle variable drop order results in compiler errors such as E0597 or E0382.
Incorrect:
fn get_ref() -> &str {
let s = String::from("option_t_data");
&s // ❌ Error E0106/E0515: returns a reference to data owned by the current function
}
Fix:
fn get_string() -> String {
let s = String::from("option_t_data");
s // Ownership of the String is transferred directly to the caller
}
Mistake 2: Mutating Option T State Without Exclusive Ownership or mut Borrowing
The mistake: Attempting to mutate data associated with Option T through an immutable reference &T or without specifying mut in variable declarations.
Why it's wrong: Rust's aliasing XOR mutability rule (&T for shared immutable access, &mut T for exclusive mutable access) prohibits mutating state through shared references unless interior mutability patterns (e.g. RefCell, Mutex) are explicitly used.
Incorrect:
fn update_val(data: &i32) {
// *data += 1; // ❌ Error E0594: cannot assign to `*data`, which is behind a `&` reference
}
Fix:
fn update_val(data: &mut i32) {
*data += 1; // Correct: exclusive mutable reference permits mutation
}
Mistake 3: Concurrent Access to Option T Across Threads Without Send / Sync Guards
The mistake: Sharing non-thread-safe Option T instances across OS threads via std::thread::spawn.
Why it's wrong: Types that do not implement Send or Sync marker traits cannot safely cross thread boundaries. The compiler prevents data races by raising compile errors E0277 (trait Send is not implemented).
Incorrect:
use std::rc::Rc;
use std::thread;
let rc = Rc::new(42);
// thread::spawn(move || { println!("{}", rc); }); // ❌ Error E0277: `Rc` cannot be sent between threads safely
Fix:
use std::sync::Arc;
use std::thread;
let arc = Arc::new(42);
thread::spawn(move || {
println!("{}", arc); // Correct: `Arc` implements `Send` and `Sync`
});
5. Practice Exercises
Exercise 1: High-Frequency Trading Matcher — Order Book Spread & Executable Quote Pipeline
Scenario:
In financial trading systems, market order books continuously track the highest active bid (buy price) and lowest active ask (sell price) as optional values (Option<LimitOrder>). If either side of the market is empty, inactive, or lacks sufficient volume to meet liquidity constraints, quotes cannot be matched.
Task:
Implement an OrderBookPipeline to filter, combine, and process order quotes:
- Define a
LimitOrderstruct containingprice: u64,volume: u64, andis_active: bool. - Implement
compute_effective_spread(bid: Option<LimitOrder>, ask: Option<LimitOrder>, min_volume: u64) -> Option<u64>:- Use
Option::filterto discard any order whereis_activeisfalseorvolume < min_volume. - Use
Option::zipto combine the valid bid and ask into a singleOption<(LimitOrder, LimitOrder)>. - Use
Option::and_thento calculateask.price - bid.priceifask.price > bid.price, or returnNoneif the market is crossed (ask.price <= bid.price).
- Use
- Implement
extract_and_discount_best_bid(bid: &mut Option<LimitOrder>, discount_bps: u64) -> Option<u64>:- Use
Option::take()to consume ownership of the inner order out of a mutable reference, filter for activity, and calculate the discounted price using basis points (price * discount_bps / 10_000).
- Use
Answer
Implementation
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LimitOrder {
pub price: u64,
pub volume: u64,
pub is_active: bool,
}
pub struct OrderBookPipeline;
impl OrderBookPipeline {
pub fn compute_effective_spread(
bid: Option<LimitOrder>,
ask: Option<LimitOrder>,
min_volume: u64,
) -> Option<u64> {
let valid_bid = bid.filter(|b| b.is_active && b.volume >= min_volume);
let valid_ask = ask.filter(|a| a.is_active && a.volume >= min_volume);
valid_bid.zip(valid_ask).and_then(|(b, a)| {
if a.price > b.price {
Some(a.price - b.price)
} else {
None
}
})
}
pub fn extract_and_discount_best_bid(
bid: &mut Option<LimitOrder>,
discount_bps: u64,
) -> Option<u64> {
bid.take().filter(|b| b.is_active).map(|mut b| {
let discount = (b.price * discount_bps) / 10_000;
b.price.saturating_sub(discount)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_spread_computation() {
let bid = Some(LimitOrder { price: 100, volume: 50, is_active: true });
let ask = Some(LimitOrder { price: 105, volume: 60, is_active: true });
let spread = OrderBookPipeline::compute_effective_spread(bid, ask, 10);
assert_eq!(spread, Some(5));
assert!(spread.is_some());
}
#[test]
fn test_filtered_inactive_or_insufficient_volume() {
let bid = Some(LimitOrder { price: 100, volume: 5, is_active: true }); // Volume too low
let ask = Some(LimitOrder { price: 105, volume: 60, is_active: true });
let spread = OrderBookPipeline::compute_effective_spread(bid, ask, 10);
assert_eq!(spread, None);
assert!(spread.is_none());
let inactive_bid = Some(LimitOrder { price: 100, volume: 50, is_active: false });
let spread_inactive = OrderBookPipeline::compute_effective_spread(inactive_bid, ask, 10);
assert_ne!(spread_inactive, Some(5));
assert!(matches!(spread_inactive, None));
}
#[test]
fn test_extract_and_discount() {
let mut bid_opt = Some(LimitOrder { price: 1000, volume: 20, is_active: true });
let discounted_price = OrderBookPipeline::extract_and_discount_best_bid(&mut bid_opt, 500); // 5% discount
assert_eq!(discounted_price, Some(950));
assert!(bid_opt.is_none()); // Taken option leaves None behind
}
}
Technical Explanation
- Monadic Filtering with
Option::filter: The predicateb.is_active && b.volume >= min_volumeis evaluated directly on the inner value ofOption<LimitOrder>. If the predicate evaluates tofalse,filterconvertsSome(order)intoNonewithout requiring manualmatchstatements. - Option Zipping with
Option::zip: Combining two independentOptionvalues (valid_bidandvalid_ask) into a singleOption<(T, U)>guarantees that downstream logic only executes when both bid and ask sides are present (Some). If either side isNone, the zipped result immediately becomesNone. - Ownership Transfer with
Option::take(): Thebid.take()method moves the innerLimitOrderout of the&mut Option<LimitOrder>, leavingNonein its place. This avoids unnecessary copies or requiringCloneonLimitOrder, while respecting Rust's borrow checker rules for mutable references. - Edge Cases: Crossed market conditions (
ask.price <= bid.price) yieldNoneviaand_then, preventing negative spread calculations. Arithmetic overflow during discount calculation is guarded usingu64::saturating_sub.
Exercise 2: Network Protocol Header Negotiation — TCP Option Parameter Parsing & Fallback Engine
Scenario: During a TCP handshake (SYN/SYN-ACK), network endpoints exchange optional configuration fields such as Maximum Segment Size (MSS), Window Scale Factor, and Selective ACK (SACK) permissions. Each parameter is optional, and handshake negotiations must determine safe mutually agreed-upon defaults when fields are omitted.
Task:
Implement a TcpNegotiator pipeline:
- Define
TcpHeaderOptionscontainingmss: Option<u16>,window_scale: Option<u8>, andsack_permitted: Option<bool>. - Define
NegotiatedConfigcontainingeffective_mss: u16,window_scale: Option<u8>, andsack_enabled: bool. - Implement
negotiate_config(client: &TcpHeaderOptions, server: &TcpHeaderOptions) -> NegotiatedConfig:- Determine
effective_mss: If both present (viaclient.mss.zip(server.mss)), choose the minimum value. If only one is present, fallback using.or_else()chain. If neither is present, fallback to standard default536using.unwrap_or(). - Determine
window_scale: Negotiate only if both client and server provide a scale factor (choose minimum); otherwise evaluate toNone. - Determine
sack_enabled: Set totrueif and only if both client and server explicitly setsack_permittedtoSome(true).
- Determine
- Implement
inspect_and_reset_option(option_slot: &mut Option<u16>) -> Option<u16>to consume active options above zero while resetting the slot toNone.
Answer
Implementation
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TcpHeaderOptions {
pub mss: Option<u16>,
pub window_scale: Option<u8>,
pub sack_permitted: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NegotiatedConfig {
pub effective_mss: u16,
pub window_scale: Option<u8>,
pub sack_enabled: bool,
}
pub struct TcpNegotiator;
impl TcpNegotiator {
pub fn negotiate_config(
client: &TcpHeaderOptions,
server: &TcpHeaderOptions,
) -> NegotiatedConfig {
const DEFAULT_MSS: u16 = 536;
let effective_mss = client
.mss
.zip(server.mss)
.map(|(c, s)| std::cmp::min(c, s))
.or_else(|| client.mss)
.or_else(|| server.mss)
.unwrap_or(DEFAULT_MSS);
let window_scale = client
.window_scale
.zip(server.window_scale)
.map(|(c, s)| std::cmp::min(c, s));
let sack_enabled = client
.sack_permitted
.zip(server.sack_permitted)
.map(|(c, s)| c && s)
.unwrap_or(false);
NegotiatedConfig {
effective_mss,
window_scale,
sack_enabled,
}
}
pub fn inspect_and_reset_option(option_slot: &mut Option<u16>) -> Option<u16> {
option_slot.take().filter(|&val| val > 0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_negotiate_full_options() {
let client = TcpHeaderOptions {
mss: Some(1460),
window_scale: Some(7),
sack_permitted: Some(true),
};
let server = TcpHeaderOptions {
mss: Some(1400),
window_scale: Some(5),
sack_permitted: Some(true),
};
let config = TcpNegotiator::negotiate_config(&client, &server);
assert_eq!(config.effective_mss, 1400);
assert_eq!(config.window_scale, Some(5));
assert!(config.sack_enabled);
}
#[test]
fn test_negotiate_fallback_defaults() {
let client = TcpHeaderOptions {
mss: Some(1460),
window_scale: None,
sack_permitted: Some(true),
};
let server = TcpHeaderOptions {
mss: None,
window_scale: Some(3),
sack_permitted: Some(false),
};
let config = TcpNegotiator::negotiate_config(&client, &server);
assert_eq!(config.effective_mss, 1460);
assert_ne!(config.effective_mss, 536);
assert!(matches!(config.window_scale, None));
assert!(!config.sack_enabled);
}
#[test]
fn test_option_reset() {
let mut slot = Some(1024);
let val = TcpNegotiator::inspect_and_reset_option(&mut slot);
assert_eq!(val, Some(1024));
assert!(slot.is_none());
}
}
Technical Explanation
- Fallback Chaining with
.or_else()and.unwrap_or(): Ineffective_mss,ziphandles the case where both parameters are present. If either is missing,.or_else(|| client.mss)and.or_else(|| server.mss)lazily evaluate alternativeOptionbranches without eager allocation. Finally,.unwrap_or(DEFAULT_MSS)provides a guaranteed non-optional primitive value. - Boolean Combination via Monadic
.map():sack_permittedrelies on.zip()to verify both options areSome, mapping the tuple(c, s)to logicalc && s. The trailing.unwrap_or(false)safely defaults missing flags tofalse. - In-place State Reset via
Option::take(): Theinspect_and_reset_optionfunction extracts the inneru16usingtake(), leavingNonein the source location. It then chains.filter(|&val| val > 0)to ensure zero-valued options are treated as invalid/absent (None). - Safety & Zero-Cost Abstractions: Option operations like
zip,map, andor_elsecompile down to straightforward conditional jumps in assembly, matching C-style null checks in performance while providing 100% compile-time safety.
Exercise 3: AST Expression Evaluator & Lexical Scope Environment Lookup
Scenario: In compiler design and script execution engines, variable resolution scans stack frames from innermost (local scope) to outermost (global scope). Expression evaluation over Abstract Syntax Trees (ASTs) must propagate missing variable errors or divide-by-zero occurrences without crashing the host process.
Task: Implement an AST evaluator with hierarchical environment resolution:
- Define an
Exprenum:Literal(i64),Variable(String),Add(Box<Expr>, Box<Expr>), andSafeDiv(Box<Expr>, Box<Expr>). - Define
EnvironmentChainholdingscopes: Vec<HashMap<String, i64>>. - Implement
lookup(&self, name: &str) -> Option<i64>: iterate backwards through scope frames using.iter().rev()and.find_map()to find the first matching key. - Implement
eval_expr(&self, expr: &Expr) -> Option<i64>:- For
Literal, wrap value inSome. - For
Variable, look up symbol in environment. - For
Add, recursively evaluate left and right sub-expressions, combining them using.zip()and.map(). - For
SafeDiv, recursively evaluate numerator and denominator, zip them, and use.and_then()to returnNoneif denominator is0.
- For
Answer
Implementation
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
Literal(i64),
Variable(String),
Add(Box<Expr>, Box<Expr>),
SafeDiv(Box<Expr>, Box<Expr>),
}
#[derive(Debug, Default)]
pub struct EnvironmentChain {
scopes: Vec<HashMap<String, i64>>,
}
impl EnvironmentChain {
pub fn new() -> Self {
Self { scopes: Vec::new() }
}
pub fn push_scope(&mut self, scope: HashMap<String, i64>) {
self.scopes.push(scope);
}
pub fn lookup(&self, name: &str) -> Option<i64> {
self.scopes
.iter()
.rev()
.find_map(|scope| scope.get(name).copied())
}
pub fn eval_expr(&self, expr: &Expr) -> Option<i64> {
match expr {
Expr::Literal(val) => Some(*val),
Expr::Variable(name) => self.lookup(name),
Expr::Add(left, right) => {
let l = self.eval_expr(left);
let r = self.eval_expr(right);
l.zip(r).map(|(a, b)| a + b)
}
Expr::SafeDiv(numerator, denominator) => {
let num = self.eval_expr(numerator);
let den = self.eval_expr(denominator);
num.zip(den).and_then(|(n, d)| {
if d != 0 {
Some(n / d)
} else {
None
}
})
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scope_shadowing_and_lookup() {
let mut env = EnvironmentChain::new();
let mut global_scope = HashMap::new();
global_scope.insert("x".to_string(), 10);
global_scope.insert("y".to_string(), 20);
env.push_scope(global_scope);
let mut local_scope = HashMap::new();
local_scope.insert("x".to_string(), 99); // Shadowing 'x'
env.push_scope(local_scope);
assert_eq!(env.lookup("x"), Some(99));
assert_eq!(env.lookup("y"), Some(20));
assert!(env.lookup("z").is_none());
}
#[test]
fn test_expression_eval_success() {
let mut env = EnvironmentChain::new();
let mut scope = HashMap::new();
scope.insert("base".to_string(), 100);
env.push_scope(scope);
// Expression: base + (50 / 2)
let expr = Expr::Add(
Box::new(Expr::Variable("base".to_string())),
Box::new(Expr::SafeDiv(
Box::new(Expr::Literal(50)),
Box::new(Expr::Literal(2)),
)),
);
let result = env.eval_expr(&expr);
assert_eq!(result, Some(125));
assert!(result.is_some());
}
#[test]
fn test_expression_eval_division_by_zero_and_missing_var() {
let env = EnvironmentChain::new();
// Division by zero
let div_by_zero = Expr::SafeDiv(
Box::new(Expr::Literal(10)),
Box::new(Expr::Literal(0)),
);
let res_zero = env.eval_expr(&div_by_zero);
assert_eq!(res_zero, None);
assert!(matches!(res_zero, None));
// Missing variable lookup
let missing_var = Expr::Add(
Box::new(Expr::Variable("missing".to_string())),
Box::new(Expr::Literal(5)),
);
let res_missing = env.eval_expr(&missing_var);
assert_ne!(res_missing, Some(5));
assert!(res_missing.is_none());
}
}
Technical Explanation
- Short-Circuit Shortening with
OptionChaining: When evaluating complex nested expressions, any inner evaluation failure (such as an unbound variable or division by zero) producesNone. Monadic operations like.zip()and.and_then()propagateNoneautomatically up the evaluation tree without panic or explicit error checking at every level. - Lexical Scope Traversal with
Iterator::find_map:scopes.iter().rev()iterates backward from local to outer scopes. Callingscope.get(name).copied()convertsOption<&i64>toOption<i64>, andfind_mapreturns the firstSome(val)encountered, correctly enforcing variable shadowing rules. - Safe Division Invariants: Mathematical undefined operations (division by zero) are captured within
and_then(|(n, d)| if d != 0 { Some(n / d) } else { None }), turning dynamic runtime arithmetic exceptions into safe, handled missing values. - Recursive AST Traversal: Passing references
&Exprdown the AST tree ensures zero dynamic memory allocations during evaluation. The compiler verifies lifetimes, ensuring references toEnvironmentChainremain valid across the call hierarchy.
6. Related Terms
Result<T, E>— The other famous built-in enum, used for error handling (Success vs Failure) rather than missing values (Present vs Absent).unwrap()/expect()— Methods used to aggressively extract the value from anOption, intentionally crashing the program if it isNone.- Integer Overflow Semantics (
checked_/wrapping_/saturating_/overflowing_) — Related concept: Integer Overflow Semantics (checked_/wrapping_/saturating_/overflowing_). - Enum — Related concept: Enum.
HashMap<K, V>— Related concept:HashMap<K, V>.Vec<T>— Related concept:Vec<T>.DefaultTrait — Related concept:DefaultTrait.if let/while let— Related concept:if let/while let.
7. Key Takeaways
- Rust does not have
null. It uses theOption<T>enum to represent the concept of absence. - The two variants are
Some(value)(data is present) andNone(data is missing). - The compiler forces you to handle the
Nonepossibility, making "null pointer exceptions" impossible in safe Rust code. - You must "open the box" using
matchorif letto safely extract and use the data hidden insideSome.