Tuple Pattern Matching
Tutorial Video
Text description (accessibility)
This video demonstrates the "Tuple Pattern Matching" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Many decisions depend on the combination of multiple conditions. Key difference from OCaml: 1. **Syntax**: Rust `match (a, b)` and OCaml `match (a, b) with` are nearly identical — the pattern is universal across ML
Tutorial
The Problem
Many decisions depend on the combination of multiple conditions. FizzBuzz is the canonical example: the output depends on two independent Boolean conditions. Without tuple matching, you need nested if statements. Matching on a tuple (cond1, cond2) expresses the decision matrix declaratively: each arm covers exactly one combination. This pattern is used in state transition tables, game logic, protocol state machines, and any logic where multiple independent conditions determine the outcome.
🎯 Learning Outcomes
match (a, b) { (true, true) => ... } matches all combinations of two conditionsmatch (opt_a, opt_b) { (Some(a), Some(b)) => ... }_ in tuple positions allows partial matching: (true, _) => ...Code Example
fn fizzbuzz(n: u32) -> String {
match (n % 3 == 0, n % 5 == 0) {
(true, true) => "FizzBuzz".into(),
(true, false) => "Fizz".into(),
(false, true) => "Buzz".into(),
(false, false) => n.to_string(),
}
}Key Differences
match (a, b) and OCaml match (a, b) with are nearly identical — the pattern is universal across ML-family languages.if/else chains obscure the structure.(bool, bool) combinations are covered — adding a case for (_, _) or relying on exhaustiveness is clear.(a, b) expression has zero heap overhead; OCaml tuples are heap-allocated GC values.OCaml Approach
OCaml tuple pattern matching is identical:
let fizzbuzz n = match (n mod 3 = 0, n mod 5 = 0) with
| (true, true) -> "FizzBuzz"
| (true, false) -> "Fizz"
| (false, true) -> "Buzz"
| (false, false) -> string_of_int n
This is one of the most natural examples of OCaml pattern matching — the code reads exactly like a truth table.
Full Source
#![allow(clippy::all)]
//! # Tuple Pattern Matching
//!
//! Match on multiple values simultaneously using tuple patterns.
/// FizzBuzz using tuple pattern matching.
pub fn fizzbuzz(n: u32) -> String {
match (n % 3 == 0, n % 5 == 0) {
(true, true) => "FizzBuzz".into(),
(true, false) => "Fizz".into(),
(false, true) => "Buzz".into(),
(false, false) => n.to_string(),
}
}
/// Alternative using if-else chain.
pub fn fizzbuzz_if(n: u32) -> String {
if n % 3 == 0 && n % 5 == 0 {
"FizzBuzz".into()
} else if n % 3 == 0 {
"Fizz".into()
} else if n % 5 == 0 {
"Buzz".into()
} else {
n.to_string()
}
}
/// Traffic light state.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Light {
Red,
Yellow,
Green,
}
/// State machine for traffic light with emergency override.
pub fn next_light(light: Light, emergency: bool) -> Light {
match (light, emergency) {
(_, true) => Light::Red,
(Light::Red, false) => Light::Green,
(Light::Green, false) => Light::Yellow,
(Light::Yellow, false) => Light::Red,
}
}
/// Compare two values and return ordering description.
pub fn compare(a: i32, b: i32) -> &'static str {
match (a > b, a < b) {
(true, false) => "greater",
(false, true) => "less",
_ => "equal",
}
}
/// Alternative using Ordering.
pub fn compare_ord(a: i32, b: i32) -> &'static str {
match a.cmp(&b) {
std::cmp::Ordering::Greater => "greater",
std::cmp::Ordering::Less => "less",
std::cmp::Ordering::Equal => "equal",
}
}
/// Match on three boolean conditions.
pub fn classify_triple(a: bool, b: bool, c: bool) -> &'static str {
match (a, b, c) {
(true, true, true) => "all true",
(false, false, false) => "all false",
(true, _, _) => "a is true",
(_, true, _) => "b is true",
(_, _, true) => "c is true",
_ => "unreachable",
}
}
/// Point classification using tuple matching.
pub fn quadrant(x: i32, y: i32) -> &'static str {
match (x.signum(), y.signum()) {
(1, 1) => "Q1",
(-1, 1) => "Q2",
(-1, -1) => "Q3",
(1, -1) => "Q4",
(0, _) | (_, 0) => "axis",
_ => "origin",
}
}
/// Match on Option pair.
pub fn both_some<T, U>(a: Option<T>, b: Option<U>) -> bool {
matches!((a, b), (Some(_), Some(_)))
}
/// Extract values from Option pair.
pub fn extract_pair<T, U>(a: Option<T>, b: Option<U>) -> Option<(T, U)> {
match (a, b) {
(Some(x), Some(y)) => Some((x, y)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fizzbuzz() {
assert_eq!(fizzbuzz(1), "1");
assert_eq!(fizzbuzz(3), "Fizz");
assert_eq!(fizzbuzz(5), "Buzz");
assert_eq!(fizzbuzz(15), "FizzBuzz");
assert_eq!(fizzbuzz(7), "7");
}
#[test]
fn test_fizzbuzz_approaches_equivalent() {
for n in 1..=30 {
assert_eq!(fizzbuzz(n), fizzbuzz_if(n));
}
}
#[test]
fn test_next_light_normal() {
assert_eq!(next_light(Light::Red, false), Light::Green);
assert_eq!(next_light(Light::Green, false), Light::Yellow);
assert_eq!(next_light(Light::Yellow, false), Light::Red);
}
#[test]
fn test_next_light_emergency() {
assert_eq!(next_light(Light::Red, true), Light::Red);
assert_eq!(next_light(Light::Green, true), Light::Red);
assert_eq!(next_light(Light::Yellow, true), Light::Red);
}
#[test]
fn test_compare() {
assert_eq!(compare(5, 3), "greater");
assert_eq!(compare(3, 5), "less");
assert_eq!(compare(4, 4), "equal");
}
#[test]
fn test_compare_approaches_equivalent() {
let cases = [(1, 2), (2, 1), (3, 3), (-1, 1), (0, 0)];
for (a, b) in cases {
assert_eq!(compare(a, b), compare_ord(a, b));
}
}
#[test]
fn test_classify_triple() {
assert_eq!(classify_triple(true, true, true), "all true");
assert_eq!(classify_triple(false, false, false), "all false");
assert_eq!(classify_triple(true, false, false), "a is true");
assert_eq!(classify_triple(false, true, false), "b is true");
assert_eq!(classify_triple(false, false, true), "c is true");
}
#[test]
fn test_quadrant() {
assert_eq!(quadrant(1, 1), "Q1");
assert_eq!(quadrant(-1, 1), "Q2");
assert_eq!(quadrant(-1, -1), "Q3");
assert_eq!(quadrant(1, -1), "Q4");
assert_eq!(quadrant(0, 5), "axis");
assert_eq!(quadrant(5, 0), "axis");
}
#[test]
fn test_both_some() {
assert!(both_some(Some(1), Some(2)));
assert!(!both_some(Some(1), None::<i32>));
assert!(!both_some(None::<i32>, Some(2)));
assert!(!both_some(None::<i32>, None::<i32>));
}
#[test]
fn test_extract_pair() {
assert_eq!(extract_pair(Some(1), Some("a")), Some((1, "a")));
assert_eq!(extract_pair(Some(1), None::<&str>), None);
assert_eq!(extract_pair(None::<i32>, Some("a")), None);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fizzbuzz() {
assert_eq!(fizzbuzz(1), "1");
assert_eq!(fizzbuzz(3), "Fizz");
assert_eq!(fizzbuzz(5), "Buzz");
assert_eq!(fizzbuzz(15), "FizzBuzz");
assert_eq!(fizzbuzz(7), "7");
}
#[test]
fn test_fizzbuzz_approaches_equivalent() {
for n in 1..=30 {
assert_eq!(fizzbuzz(n), fizzbuzz_if(n));
}
}
#[test]
fn test_next_light_normal() {
assert_eq!(next_light(Light::Red, false), Light::Green);
assert_eq!(next_light(Light::Green, false), Light::Yellow);
assert_eq!(next_light(Light::Yellow, false), Light::Red);
}
#[test]
fn test_next_light_emergency() {
assert_eq!(next_light(Light::Red, true), Light::Red);
assert_eq!(next_light(Light::Green, true), Light::Red);
assert_eq!(next_light(Light::Yellow, true), Light::Red);
}
#[test]
fn test_compare() {
assert_eq!(compare(5, 3), "greater");
assert_eq!(compare(3, 5), "less");
assert_eq!(compare(4, 4), "equal");
}
#[test]
fn test_compare_approaches_equivalent() {
let cases = [(1, 2), (2, 1), (3, 3), (-1, 1), (0, 0)];
for (a, b) in cases {
assert_eq!(compare(a, b), compare_ord(a, b));
}
}
#[test]
fn test_classify_triple() {
assert_eq!(classify_triple(true, true, true), "all true");
assert_eq!(classify_triple(false, false, false), "all false");
assert_eq!(classify_triple(true, false, false), "a is true");
assert_eq!(classify_triple(false, true, false), "b is true");
assert_eq!(classify_triple(false, false, true), "c is true");
}
#[test]
fn test_quadrant() {
assert_eq!(quadrant(1, 1), "Q1");
assert_eq!(quadrant(-1, 1), "Q2");
assert_eq!(quadrant(-1, -1), "Q3");
assert_eq!(quadrant(1, -1), "Q4");
assert_eq!(quadrant(0, 5), "axis");
assert_eq!(quadrant(5, 0), "axis");
}
#[test]
fn test_both_some() {
assert!(both_some(Some(1), Some(2)));
assert!(!both_some(Some(1), None::<i32>));
assert!(!both_some(None::<i32>, Some(2)));
assert!(!both_some(None::<i32>, None::<i32>));
}
#[test]
fn test_extract_pair() {
assert_eq!(extract_pair(Some(1), Some("a")), Some((1, "a")));
assert_eq!(extract_pair(Some(1), None::<&str>), None);
assert_eq!(extract_pair(None::<i32>, Some("a")), None);
}
}
Deep Comparison
OCaml vs Rust: Tuple Pattern Matching
FizzBuzz Example
OCaml
let fizzbuzz n = match (n mod 3 = 0, n mod 5 = 0) with
| (true, true) -> "FizzBuzz"
| (true, false) -> "Fizz"
| (false, true) -> "Buzz"
| (false, false) -> string_of_int n
Rust
fn fizzbuzz(n: u32) -> String {
match (n % 3 == 0, n % 5 == 0) {
(true, true) => "FizzBuzz".into(),
(true, false) => "Fizz".into(),
(false, true) => "Buzz".into(),
(false, false) => n.to_string(),
}
}
State Machine with Tuple
OCaml
type light = Red | Yellow | Green
let next (l, emergency) = match (l, emergency) with
| (_, true) -> Red
| (Red, false) -> Green
| (Green, false) -> Yellow
| (Yellow, false) -> Red
Rust
fn next_light(light: Light, emergency: bool) -> Light {
match (light, emergency) {
(_, true) => Light::Red,
(Light::Red, false) => Light::Green,
(Light::Green, false) => Light::Yellow,
(Light::Yellow, false)=> Light::Red,
}
}
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Syntax | match (a, b) with | match (a, b) { } |
| Wildcard | _ | _ |
| Arrow | -> | => |
| Pattern guards | when condition | if condition |
Benefits of Tuple Matching
Exercises
(n%3==0, n%5==0, n%7==0) and use _ to collapse irrelevant combinations.fn transition(state: State, event: Event) -> State for a traffic light using tuple matching on (state, event).fn combine(a: Option<i32>, b: Option<i32>) -> Option<i32> that returns Some(a + b) if both are Some, Some(a) if only a, Some(b) if only b, None otherwise.