Example 062: Records — Immutable Update and Pattern Matching
Difficulty: ⭐ Category: Algebraic Data Types Concept: Named product types (records/structs) with functional update syntax. Both OCaml and Rust support creating modified copies of records without mutation, making immutable data structures ergonomic and expressive. OCaml → Rust insight: OCaml's `{ r with field = value }` maps directly to Rust's `Struct { field: value, ..old }` — both are syntactic sugar for constructing a new value reusing unchanged fields./// 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 }));
}
}
fn main() {
println!("{:?}", (area(&sample_rect()) - 50.0).abs() < f64::EPSILON);
println!("{:?}", (perimeter(&sample_rect()) - 30.0).abs() < f64::EPSILON);
println!("{:?}", (r2.origin.x - 3.0).abs() < f64::EPSILON);
}
type point = { x : float; y : float }
type rect = { origin : point; width : float; height : float }
let area { width; height; _ } = width *. height
let perimeter { width; height; _ } = 2.0 *. (width +. height)
let translate dx dy r =
{ r with origin = { x = r.origin.x +. dx; y = r.origin.y +. dy } }
let contains_point r { x; y } =
x >= r.origin.x && x <= r.origin.x +. r.width &&
y >= r.origin.y && y <= r.origin.y +. r.height
let () =
let r = { origin = { x = 0.0; y = 0.0 }; width = 10.0; height = 5.0 } in
assert (area r = 50.0);
assert (perimeter r = 30.0);
let r2 = translate 3.0 4.0 r in
assert (r2.origin.x = 3.0);
assert (contains_point r { x = 1.0; y = 1.0 });
assert (not (contains_point r { x = 11.0; y = 1.0 }));
print_endline "All assertions passed."
📊 Detailed 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
- Rust's `..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
- Rust's `&Rect` borrowing lets functions read a record without taking ownership, similar to how OCaml freely passes GC-managed values
- Float comparison in Rust requires epsilon checks (`(a - b).abs() < f64::EPSILON`) — there's no built-in structural equality for floats
- Visibility is more granular in Rust: each field can be independently `pub` or private
Further Reading
- [The Rust Book — Structs](https://doc.rust-lang.org/book/ch05-01-defining-structs.html)
- [The Rust Book — Struct Update Syntax](https://doc.rust-lang.org/book/ch05-01-defining-structs.html#creating-instances-from-other-instances-with-struct-update-syntax)
- [OCaml Records](https://cs3110.github.io/textbook/chapters/data/records_tuples.html)