🦀 Functional Rust

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

ConceptOCamlRust
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;`
MemoryGC heapStack (Copy) or heap (Box/Vec)
MutabilityImmutable default, opt-in `mutable`Immutable default, opt-in `mut`
VisibilityModule-levelPer-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)