Tuple Struct Patterns
Tutorial Video
Text description (accessibility)
This video demonstrates the "Tuple Struct Patterns" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Tuple structs serve two roles: lightweight structs with positional fields, and newtypes — single-field wrappers that create type-distinct values. Key difference from OCaml: 1. **Syntax**: Rust `struct Meters(f64)` defines a tuple struct; OCaml `type meters = Meters of float` defines a single
Tutorial
The Problem
Tuple structs serve two roles: lightweight structs with positional fields, and newtypes — single-field wrappers that create type-distinct values. The newtype pattern (struct Meters(f64)) prevents accidentally passing Seconds where Meters is expected, even though both are f64 internally. Pattern matching on tuple structs extracts fields directly, either in match arms, let bindings, or function parameters. This is common in unit systems (NASA Mars Climate Orbiter crashed from a meters/feet confusion), domain modeling, and type-safe ID types.
🎯 Learning Outcomes
let Point(x, y) = p; destructures a tuple structfn f(Point(x, y): &Point) puts destructuring directly in function parametersMeters(m) in a pattern extracts the inner value while discarding the wrapperstruct Meters(f64)) prevents type confusion at compile timeCode Example
#![allow(clippy::all)]
//! Tuple Struct Patterns
//!
//! Destructuring tuple structs and newtypes.
pub struct Point(pub i32, pub i32);
pub struct Color(pub u8, pub u8, pub u8);
pub struct Meters(pub f64);
pub struct Seconds(pub f64);
/// Destructure tuple struct.
pub fn get_coords(p: &Point) -> (i32, i32) {
let Point(x, y) = p;
(*x, *y)
}
/// Pattern in function params.
pub fn add_points(Point(x1, y1): &Point, Point(x2, y2): &Point) -> Point {
Point(x1 + x2, y1 + y2)
}
/// Newtype pattern.
pub fn meters_to_feet(Meters(m): Meters) -> f64 {
m * 3.28084
}
/// Match with tuple struct.
pub fn describe_color(Color(r, g, b): &Color) -> &'static str {
match (r, g, b) {
(255, 0, 0) => "red",
(0, 255, 0) => "green",
(0, 0, 255) => "blue",
(0, 0, 0) => "black",
(255, 255, 255) => "white",
_ => "other",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_coords() {
let p = Point(3, 4);
assert_eq!(get_coords(&p), (3, 4));
}
#[test]
fn test_add_points() {
let p = add_points(&Point(1, 2), &Point(3, 4));
assert_eq!((p.0, p.1), (4, 6));
}
#[test]
fn test_meters() {
let ft = meters_to_feet(Meters(1.0));
assert!((ft - 3.28084).abs() < 0.001);
}
#[test]
fn test_color() {
assert_eq!(describe_color(&Color(255, 0, 0)), "red");
assert_eq!(describe_color(&Color(128, 128, 128)), "other");
}
}Key Differences
struct Meters(f64) defines a tuple struct; OCaml type meters = Meters of float defines a single-constructor variant — both serve the newtype purpose.match on a single-constructor variant is always exhaustive; Rust let Meters(m) = val is irrefutable (always succeeds).Meters and Seconds — a compile error in both languages.pub or private; OCaml's abstraction is controlled via module signatures.OCaml Approach
OCaml achieves newtypes with abstract module signatures or single-constructor variants:
type meters = Meters of float
type seconds = Seconds of float
let meters_to_feet (Meters m) = m *. 3.28084
The Meters of float pattern is identical in concept to Rust's struct Meters(f64).
Full Source
#![allow(clippy::all)]
//! Tuple Struct Patterns
//!
//! Destructuring tuple structs and newtypes.
pub struct Point(pub i32, pub i32);
pub struct Color(pub u8, pub u8, pub u8);
pub struct Meters(pub f64);
pub struct Seconds(pub f64);
/// Destructure tuple struct.
pub fn get_coords(p: &Point) -> (i32, i32) {
let Point(x, y) = p;
(*x, *y)
}
/// Pattern in function params.
pub fn add_points(Point(x1, y1): &Point, Point(x2, y2): &Point) -> Point {
Point(x1 + x2, y1 + y2)
}
/// Newtype pattern.
pub fn meters_to_feet(Meters(m): Meters) -> f64 {
m * 3.28084
}
/// Match with tuple struct.
pub fn describe_color(Color(r, g, b): &Color) -> &'static str {
match (r, g, b) {
(255, 0, 0) => "red",
(0, 255, 0) => "green",
(0, 0, 255) => "blue",
(0, 0, 0) => "black",
(255, 255, 255) => "white",
_ => "other",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_coords() {
let p = Point(3, 4);
assert_eq!(get_coords(&p), (3, 4));
}
#[test]
fn test_add_points() {
let p = add_points(&Point(1, 2), &Point(3, 4));
assert_eq!((p.0, p.1), (4, 6));
}
#[test]
fn test_meters() {
let ft = meters_to_feet(Meters(1.0));
assert!((ft - 3.28084).abs() < 0.001);
}
#[test]
fn test_color() {
assert_eq!(describe_color(&Color(255, 0, 0)), "red");
assert_eq!(describe_color(&Color(128, 128, 128)), "other");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_coords() {
let p = Point(3, 4);
assert_eq!(get_coords(&p), (3, 4));
}
#[test]
fn test_add_points() {
let p = add_points(&Point(1, 2), &Point(3, 4));
assert_eq!((p.0, p.1), (4, 6));
}
#[test]
fn test_meters() {
let ft = meters_to_feet(Meters(1.0));
assert!((ft - 3.28084).abs() < 0.001);
}
#[test]
fn test_color() {
assert_eq!(describe_color(&Color(255, 0, 0)), "red");
assert_eq!(describe_color(&Color(128, 128, 128)), "other");
}
}
Deep Comparison
OCaml vs Rust: pattern tuple struct
See example.rs and example.ml for implementations.
Exercises
struct Celsius(f64) and struct Fahrenheit(f64) with conversion functions that destructure in parameters; verify the compiler rejects passing Celsius to a Fahrenheit function.struct UserId(u64) and struct PostId(u64) and write a get_post_for_user(uid: UserId, pid: PostId) -> String function — verify (pid, uid) argument order fails at compile time.struct Range(i32, i32) with methods contains(&self, n: i32) -> bool and overlap(&self, Range(other_start, other_end): &Range) -> bool using tuple struct destructuring.