ExamplesBy LevelBy TopicLearning Paths
931 Intermediate

931-records — Records and Functional Update

Functional Programming

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

  • • Define structs with named fields as Rust's equivalent of OCaml records
  • • Use struct update syntax Struct { field: value, ..old } for functional update
  • • Destructure structs in function parameters (pattern matching on structs)
  • • Implement immutable operations that return new values rather than mutating
  • • Compare Rust's ..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

  • Syntax: OCaml { r with field = v } vs Rust Struct { field: v, ..r }. Both copy unchanged fields; Rust lists fields before ..old, OCaml uses with.
  • Copy semantics: Rust structs must implement Copy for the .. update to work without moving; OCaml records are always shareable via GC.
  • Nested updates: OCaml's nested with is more concise for deeply nested updates; Rust requires explicit reconstruction at each nesting level.
  • Mutable fields: OCaml has explicit 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 }));
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptOCamlRust
    Definitiontype point = { x: float; y: float }struct Point { x: f64, y: f64 }
    Functional update{ r with x = 5.0 }Point { x: 5.0, ..r }
    Destructuringlet { x; y; _ } = plet Point { x, y } = p;
    MemoryGC heapStack (Copy) or heap (Box/Vec)
    MutabilityImmutable default, opt-in mutableImmutable 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)
  • Exercises

  • Add a scale(factor: f64, r: &Rect) -> Rect function that scales width and height while keeping the origin fixed.
  • Implement expand(dx: f64, dy: f64, r: &Rect) -> Rect that grows the rect symmetrically around its center.
  • Create a merge_rects(a: &Rect, b: &Rect) -> Rect that returns the smallest bounding rect containing both input rects.
  • Open Source Repos