931-records — Records and Functional Update
Tutorial
The Problem
Functional programming favors immutable data structures with "functional update": instead of modifying a record in place, you create a new record with the changed field and the rest copied from the original. This preserves the original value, enables easy undo/redo, and is safe across threads. OCaml's { r with field = new_value } syntax makes this concise. Rust provides the identical idiom with struct update syntax: Struct { field: new_value, ..old }. Both syntaxes copy unchanged fields from the original struct, creating a new value without mutation.
🎯 Learning Outcomes
Struct { field: value, ..old } for functional update..old update syntax with OCaml's { r with field = value }Code Example
#![allow(clippy::all)]
/// Records — Immutable Update and Pattern Matching
///
/// OCaml's `{ r with field = value }` functional update syntax maps directly
/// to Rust's struct update syntax `Struct { field: value, ..old }`.
/// Both create a new value without mutating the original.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point {
pub x: f64,
pub y: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rect {
pub origin: Point,
pub width: f64,
pub height: f64,
}
/// Area via destructuring — mirrors OCaml's `let area { width; height; _ }`.
pub fn area(r: &Rect) -> f64 {
r.width * r.height
}
pub fn perimeter(r: &Rect) -> f64 {
2.0 * (r.width + r.height)
}
/// Functional update: creates a new Rect with a shifted origin.
/// Uses Rust's `..r` struct update syntax, analogous to OCaml's `{ r with ... }`.
pub fn translate(dx: f64, dy: f64, r: &Rect) -> Rect {
Rect {
origin: Point {
x: r.origin.x + dx,
y: r.origin.y + dy,
},
..*r
}
}
pub fn contains_point(r: &Rect, p: &Point) -> bool {
p.x >= r.origin.x
&& p.x <= r.origin.x + r.width
&& p.y >= r.origin.y
&& p.y <= r.origin.y + r.height
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_rect() -> Rect {
Rect {
origin: Point { x: 0.0, y: 0.0 },
width: 10.0,
height: 5.0,
}
}
#[test]
fn test_area() {
assert!((area(&sample_rect()) - 50.0).abs() < f64::EPSILON);
}
#[test]
fn test_perimeter() {
assert!((perimeter(&sample_rect()) - 30.0).abs() < f64::EPSILON);
}
#[test]
fn test_translate() {
let r2 = translate(3.0, 4.0, &sample_rect());
assert!((r2.origin.x - 3.0).abs() < f64::EPSILON);
assert!((r2.origin.y - 4.0).abs() < f64::EPSILON);
assert!((r2.width - 10.0).abs() < f64::EPSILON);
}
#[test]
fn test_contains_point() {
let r = sample_rect();
assert!(contains_point(&r, &Point { x: 1.0, y: 1.0 }));
assert!(!contains_point(&r, &Point { x: 11.0, y: 1.0 }));
assert!(contains_point(&r, &Point { x: 0.0, y: 0.0 })); // edge
assert!(contains_point(&r, &Point { x: 10.0, y: 5.0 })); // corner
}
#[test]
fn test_immutability() {
let r = sample_rect();
let r2 = translate(1.0, 1.0, &r);
// Original unchanged — Rust's Copy trait means no move
assert!((r.origin.x - 0.0).abs() < f64::EPSILON);
assert!((r2.origin.x - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_zero_size_rect() {
let r = Rect {
origin: Point { x: 5.0, y: 5.0 },
width: 0.0,
height: 0.0,
};
assert!((area(&r)).abs() < f64::EPSILON);
assert!(contains_point(&r, &Point { x: 5.0, y: 5.0 }));
}
}Key Differences
{ r with field = v } vs Rust Struct { field: v, ..r }. Both copy unchanged fields; Rust lists fields before ..old, OCaml uses with.Copy for the .. update to work without moving; OCaml records are always shareable via GC.with is more concise for deeply nested updates; Rust requires explicit reconstruction at each nesting level.mutable field modifiers; Rust uses let mut binding — mutation is binding-level, not field-level.OCaml Approach
OCaml records: type rect = { origin: point; width: float; height: float }. Functional update: { r with origin = { r.origin with x = r.origin.x +. dx; y = r.origin.y +. dy } }. Pattern matching on records: let area { width; height; _ } = width *. height. OCaml's syntax is slightly more concise for nested updates because of the nested with syntax. OCaml record fields are mutable by declaring mutable field: type — Rust uses separate let mut binding for mutation.
Full Source
#![allow(clippy::all)]
/// Records — Immutable Update and Pattern Matching
///
/// OCaml's `{ r with field = value }` functional update syntax maps directly
/// to Rust's struct update syntax `Struct { field: value, ..old }`.
/// Both create a new value without mutating the original.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point {
pub x: f64,
pub y: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rect {
pub origin: Point,
pub width: f64,
pub height: f64,
}
/// Area via destructuring — mirrors OCaml's `let area { width; height; _ }`.
pub fn area(r: &Rect) -> f64 {
r.width * r.height
}
pub fn perimeter(r: &Rect) -> f64 {
2.0 * (r.width + r.height)
}
/// Functional update: creates a new Rect with a shifted origin.
/// Uses Rust's `..r` struct update syntax, analogous to OCaml's `{ r with ... }`.
pub fn translate(dx: f64, dy: f64, r: &Rect) -> Rect {
Rect {
origin: Point {
x: r.origin.x + dx,
y: r.origin.y + dy,
},
..*r
}
}
pub fn contains_point(r: &Rect, p: &Point) -> bool {
p.x >= r.origin.x
&& p.x <= r.origin.x + r.width
&& p.y >= r.origin.y
&& p.y <= r.origin.y + r.height
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_rect() -> Rect {
Rect {
origin: Point { x: 0.0, y: 0.0 },
width: 10.0,
height: 5.0,
}
}
#[test]
fn test_area() {
assert!((area(&sample_rect()) - 50.0).abs() < f64::EPSILON);
}
#[test]
fn test_perimeter() {
assert!((perimeter(&sample_rect()) - 30.0).abs() < f64::EPSILON);
}
#[test]
fn test_translate() {
let r2 = translate(3.0, 4.0, &sample_rect());
assert!((r2.origin.x - 3.0).abs() < f64::EPSILON);
assert!((r2.origin.y - 4.0).abs() < f64::EPSILON);
assert!((r2.width - 10.0).abs() < f64::EPSILON);
}
#[test]
fn test_contains_point() {
let r = sample_rect();
assert!(contains_point(&r, &Point { x: 1.0, y: 1.0 }));
assert!(!contains_point(&r, &Point { x: 11.0, y: 1.0 }));
assert!(contains_point(&r, &Point { x: 0.0, y: 0.0 })); // edge
assert!(contains_point(&r, &Point { x: 10.0, y: 5.0 })); // corner
}
#[test]
fn test_immutability() {
let r = sample_rect();
let r2 = translate(1.0, 1.0, &r);
// Original unchanged — Rust's Copy trait means no move
assert!((r.origin.x - 0.0).abs() < f64::EPSILON);
assert!((r2.origin.x - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_zero_size_rect() {
let r = Rect {
origin: Point { x: 5.0, y: 5.0 },
width: 0.0,
height: 0.0,
};
assert!((area(&r)).abs() < f64::EPSILON);
assert!(contains_point(&r, &Point { x: 5.0, y: 5.0 }));
}
}#[cfg(test)]
mod tests {
use super::*;
fn sample_rect() -> Rect {
Rect {
origin: Point { x: 0.0, y: 0.0 },
width: 10.0,
height: 5.0,
}
}
#[test]
fn test_area() {
assert!((area(&sample_rect()) - 50.0).abs() < f64::EPSILON);
}
#[test]
fn test_perimeter() {
assert!((perimeter(&sample_rect()) - 30.0).abs() < f64::EPSILON);
}
#[test]
fn test_translate() {
let r2 = translate(3.0, 4.0, &sample_rect());
assert!((r2.origin.x - 3.0).abs() < f64::EPSILON);
assert!((r2.origin.y - 4.0).abs() < f64::EPSILON);
assert!((r2.width - 10.0).abs() < f64::EPSILON);
}
#[test]
fn test_contains_point() {
let r = sample_rect();
assert!(contains_point(&r, &Point { x: 1.0, y: 1.0 }));
assert!(!contains_point(&r, &Point { x: 11.0, y: 1.0 }));
assert!(contains_point(&r, &Point { x: 0.0, y: 0.0 })); // edge
assert!(contains_point(&r, &Point { x: 10.0, y: 5.0 })); // corner
}
#[test]
fn test_immutability() {
let r = sample_rect();
let r2 = translate(1.0, 1.0, &r);
// Original unchanged — Rust's Copy trait means no move
assert!((r.origin.x - 0.0).abs() < f64::EPSILON);
assert!((r2.origin.x - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_zero_size_rect() {
let r = Rect {
origin: Point { x: 5.0, y: 5.0 },
width: 0.0,
height: 0.0,
};
assert!((area(&r)).abs() < f64::EPSILON);
assert!(contains_point(&r, &Point { x: 5.0, y: 5.0 }));
}
}
Deep Comparison
Records — Immutable Update and Pattern Matching: OCaml vs Rust
The Core Insight
Records (OCaml) and structs (Rust) are the simplest compound data types — named collections of fields. Both languages provide "functional update" syntax that constructs a new value by copying most fields from an existing one, making immutable programming ergonomic without manual field-by-field copying.
OCaml Approach
OCaml records are defined with type point = { x : float; y : float }. Pattern matching directly destructures fields: let area { width; height; _ } = .... Functional update with { r with origin = ... } creates a new record reusing unchanged fields. Records are allocated on the GC heap, and the old and new records may share unchanged sub-values. All record fields are immutable by default (mutable fields require explicit mutable annotation).
Rust Approach
Rust structs serve the same purpose: struct Point { x: f64, y: f64 }. Functional update uses Struct { changed_field: val, ..old }, which moves or copies fields from old. With #[derive(Copy, Clone)], small structs like Point are stack-allocated and implicitly copied — no heap allocation or GC needed. Rust enforces visibility with pub on each field, whereas OCaml record fields are public within their module by default.
Side-by-Side
| Concept | OCaml | Rust |
|---|---|---|
| Definition | type point = { x: float; y: float } | struct Point { x: f64, y: f64 } |
| Functional update | { r with x = 5.0 } | Point { x: 5.0, ..r } |
| Destructuring | let { x; y; _ } = p | let Point { x, y } = p; |
| Memory | GC heap | Stack (Copy) or heap (Box/Vec) |
| Mutability | Immutable default, opt-in mutable | Immutable default, opt-in mut |
| Visibility | Module-level | Per-field pub |
What Rust Learners Should Notice
..old struct update syntax is the equivalent of OCaml's { r with ... } — both create new values, neither mutates#[derive(Copy, Clone)] on small structs gives you value semantics with zero overhead — the struct lives entirely on the stack&Rect borrowing lets functions read a record without taking ownership, similar to how OCaml freely passes GC-managed values(a - b).abs() < f64::EPSILON) — there's no built-in structural equality for floatspub or privateFurther Reading
Exercises
scale(factor: f64, r: &Rect) -> Rect function that scales width and height while keeping the origin fixed.expand(dx: f64, dy: f64, r: &Rect) -> Rect that grows the rect symmetrically around its center.merge_rects(a: &Rect, b: &Rect) -> Rect that returns the smallest bounding rect containing both input rects.