let-else Pattern
Tutorial Video
Text description (accessibility)
This video demonstrates the "let-else Pattern" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Deeply nested `if let` expressions create the "pyramid of doom" — code that drifts rightward with each additional unwrap. Key difference from OCaml: 1. **Syntax**: Rust `let Pattern = expr else { ... }` is linear code; OCaml requires a `match` expression, potentially creating nesting.
Tutorial
The Problem
Deeply nested if let expressions create the "pyramid of doom" — code that drifts rightward with each additional unwrap. let-else (stabilized in Rust 1.65) provides early return on pattern mismatch: if the pattern does not match, the else block must diverge (return, break, continue, or panic). This enables "railway-oriented" linear code — extract what you need at the top, return on failure, use the value in the rest of the function. It is the idiomatic Rust replacement for chains of nested if let.
🎯 Learning Outcomes
let Pattern = expr else { return; } extracts a value or exits earlylet-else flattens if let nesting to linear codelet-else works with Option, Result, slice patterns, and struct destructuringelse block must diverge: return, break, continue, panic!, or loop { ... }let-else is most useful: input validation, argument parsing, early-out functionsCode Example
#![allow(clippy::all)]
//! let-else Pattern
//!
//! Early return when pattern doesn't match.
/// Basic let-else.
pub fn get_first(v: &[i32]) -> i32 {
let [first, ..] = v else {
return -1;
};
*first
}
/// let-else with Option.
pub fn process_option(opt: Option<i32>) -> i32 {
let Some(value) = opt else {
return 0;
};
value * 2
}
/// let-else with Result.
pub fn process_result(res: Result<i32, &str>) -> i32 {
let Ok(value) = res else {
return -1;
};
value + 10
}
/// let-else with struct destructure.
pub struct Config {
pub value: Option<i32>,
}
pub fn get_config_value(c: &Config) -> i32 {
let Some(v) = c.value else {
return 0;
};
v
}
/// Multiple let-else in sequence.
pub fn parse_pair(s: &str) -> Option<(i32, i32)> {
let Some((a, b)) = s.split_once(',') else {
return None;
};
let Ok(x) = a.trim().parse() else {
return None;
};
let Ok(y) = b.trim().parse() else {
return None;
};
Some((x, y))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_first() {
assert_eq!(get_first(&[1, 2, 3]), 1);
assert_eq!(get_first(&[]), -1);
}
#[test]
fn test_process_option() {
assert_eq!(process_option(Some(5)), 10);
assert_eq!(process_option(None), 0);
}
#[test]
fn test_process_result() {
assert_eq!(process_result(Ok(5)), 15);
assert_eq!(process_result(Err("error")), -1);
}
#[test]
fn test_get_config() {
let c = Config { value: Some(42) };
assert_eq!(get_config_value(&c), 42);
}
#[test]
fn test_parse_pair() {
assert_eq!(parse_pair("1, 2"), Some((1, 2)));
assert_eq!(parse_pair("invalid"), None);
}
}Key Differences
let Pattern = expr else { ... } is linear code; OCaml requires a match expression, potentially creating nesting.else block must diverge — the compiler enforces this; OCaml's None arm can return any value (not just diverging ones).let-else makes the bound value available for the rest of the function (not just inside a then-block); OCaml match scopes the binding to each arm.let-else is praised for enabling "happy path" linear code; OCaml match at the top of a function achieves similar clarity with explicit arms.OCaml Approach
OCaml's equivalent uses match with early return via Option.value or explicit pattern:
let process_option opt =
match opt with
| None -> 0
| Some value -> value * 2
OCaml does not have let-else syntax — the match expression achieves the same semantics.
Full Source
#![allow(clippy::all)]
//! let-else Pattern
//!
//! Early return when pattern doesn't match.
/// Basic let-else.
pub fn get_first(v: &[i32]) -> i32 {
let [first, ..] = v else {
return -1;
};
*first
}
/// let-else with Option.
pub fn process_option(opt: Option<i32>) -> i32 {
let Some(value) = opt else {
return 0;
};
value * 2
}
/// let-else with Result.
pub fn process_result(res: Result<i32, &str>) -> i32 {
let Ok(value) = res else {
return -1;
};
value + 10
}
/// let-else with struct destructure.
pub struct Config {
pub value: Option<i32>,
}
pub fn get_config_value(c: &Config) -> i32 {
let Some(v) = c.value else {
return 0;
};
v
}
/// Multiple let-else in sequence.
pub fn parse_pair(s: &str) -> Option<(i32, i32)> {
let Some((a, b)) = s.split_once(',') else {
return None;
};
let Ok(x) = a.trim().parse() else {
return None;
};
let Ok(y) = b.trim().parse() else {
return None;
};
Some((x, y))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_first() {
assert_eq!(get_first(&[1, 2, 3]), 1);
assert_eq!(get_first(&[]), -1);
}
#[test]
fn test_process_option() {
assert_eq!(process_option(Some(5)), 10);
assert_eq!(process_option(None), 0);
}
#[test]
fn test_process_result() {
assert_eq!(process_result(Ok(5)), 15);
assert_eq!(process_result(Err("error")), -1);
}
#[test]
fn test_get_config() {
let c = Config { value: Some(42) };
assert_eq!(get_config_value(&c), 42);
}
#[test]
fn test_parse_pair() {
assert_eq!(parse_pair("1, 2"), Some((1, 2)));
assert_eq!(parse_pair("invalid"), None);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_first() {
assert_eq!(get_first(&[1, 2, 3]), 1);
assert_eq!(get_first(&[]), -1);
}
#[test]
fn test_process_option() {
assert_eq!(process_option(Some(5)), 10);
assert_eq!(process_option(None), 0);
}
#[test]
fn test_process_result() {
assert_eq!(process_result(Ok(5)), 15);
assert_eq!(process_result(Err("error")), -1);
}
#[test]
fn test_get_config() {
let c = Config { value: Some(42) };
assert_eq!(get_config_value(&c), 42);
}
#[test]
fn test_parse_pair() {
assert_eq!(parse_pair("1, 2"), Some((1, 2)));
assert_eq!(parse_pair("invalid"), None);
}
}
Deep Comparison
OCaml vs Rust: pattern let else
See example.rs and example.ml for implementations.
Exercises
fn parse_positive(s: &str) -> Option<u32> using let-else to unwrap str::parse::<u32>() and return None if parsing fails or the result is zero.let-else statements extracting from a Config struct — show that each extraction can use variables from previous ones.fn parse_rgb(parts: &[&str]) -> Option<(u8, u8, u8)> using let [r_str, g_str, b_str] = parts else { return None; } followed by individual parse calls.