062 — Records (Structs)
Tutorial
The Problem
Records (called structs in Rust) are the product type of a type system — a value that bundles multiple named fields. OCaml's type point = { x: float; y: float } and Rust's struct Point { x: f64, y: f64 } are direct equivalents. Records are the foundation for representing real-world entities: users, configurations, geometric shapes, network requests.
The record update syntax — creating a new record with most fields from an existing one, changing only a few — is a functional programming staple. It appears in immutable state management (Redux reducers, Elm architecture), configuration management, and "builder" patterns.
🎯 Learning Outcomes
implConfig { debug: true, ..Config::default_config() } for partial updatesDebug, Clone, Copy for common struct utilitiesCopy (small, stack-allocated values) vs Clone (heap-allocated)struct as the equivalent of OCaml records with named fields and implement methods in an impl block#[derive(Debug, Clone, PartialEq)] to auto-generate common trait implementations without boilerplateCode Example
#![allow(clippy::all)]
// 062: Records (Structs)
// Named fields, creation, update syntax, pattern matching
// Approach 1: Basic struct
#[derive(Debug, Clone, Copy)]
struct Point {
x: f64,
y: f64,
}
impl Point {
fn origin() -> Self {
Point { x: 0.0, y: 0.0 }
}
fn distance(&self, other: &Point) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
}
// Approach 2: Struct update syntax
#[derive(Debug, Clone)]
struct Config {
host: String,
port: u16,
debug: bool,
timeout: u32,
}
impl Config {
fn default_config() -> Self {
Config {
host: "localhost".to_string(),
port: 8080,
debug: false,
timeout: 30,
}
}
}
fn dev_config() -> Config {
Config {
debug: true,
port: 3000,
..Config::default_config()
}
}
fn prod_config() -> Config {
Config {
host: "prod.example.com".to_string(),
timeout: 60,
..Config::default_config()
}
}
// Approach 3: Destructuring
fn describe_config(config: &Config) -> String {
let Config {
host, port, debug, ..
} = config;
format!("{}:{}{}", host, port, if *debug { " [DEBUG]" } else { "" })
}
fn is_local(config: &Config) -> bool {
config.host == "localhost" || config.host == "127.0.0.1"
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_point() {
let o = Point::origin();
let p = Point { x: 3.0, y: 4.0 };
assert!((o.distance(&p) - 5.0).abs() < 0.001);
}
#[test]
fn test_struct_update() {
let dev = dev_config();
assert!(dev.debug);
assert_eq!(dev.port, 3000);
assert_eq!(dev.host, "localhost");
}
#[test]
fn test_prod_config() {
let prod = prod_config();
assert_eq!(prod.timeout, 60);
assert_eq!(prod.host, "prod.example.com");
}
#[test]
fn test_describe() {
assert_eq!(describe_config(&dev_config()), "localhost:3000 [DEBUG]");
}
#[test]
fn test_is_local() {
assert!(is_local(&Config::default_config()));
assert!(!is_local(&prod_config()));
}
}Key Differences
{ record with field = value }. Rust: StructName { field: value, ..record }. Both create a new record with specified fields overridden.mutable per field. Rust structs are immutable by default; let mut s = Struct {...} makes the entire binding mutable.impl blocks separate from the struct. OCaml has module-level functions; methods are a convention, not a language feature.Copy trait**: Rust's Copy trait marks types that can be copied by value on assignment (stack-only types). OCaml's uniform representation means all values are either boxed (heap) or unboxed (stack) based on size, without explicit marking.type point = { x: float; y: float } and Rust's struct Point { x: f64, y: f64 } are isomorphic. Both are product types with named fields.{ p with x = 1.0 } creates a new record with one field changed. Rust has no built-in equivalent — use Point { x: 1.0, ..p } (struct update syntax).#[derive(Debug, Clone, PartialEq)] in Rust auto-generates common impls. OCaml uses [@@deriving show, eq] (ppx_deriving) for the same effect.let Point { x, y } = point in Rust extracts fields by name. OCaml: let { x; y } = point. Both support nested destructuring in patterns.OCaml Approach
OCaml record: type point = { x: float; y: float }. Record creation: { x = 1.0; y = 2.0 }. Update syntax: { config with debug = true; port = 3000 } — directly parallel to Rust's ..config. Pattern matching: let { x; y } = point in .... OCaml records are immutable by default; mutable fields use mutable x: float.
Full Source
#![allow(clippy::all)]
// 062: Records (Structs)
// Named fields, creation, update syntax, pattern matching
// Approach 1: Basic struct
#[derive(Debug, Clone, Copy)]
struct Point {
x: f64,
y: f64,
}
impl Point {
fn origin() -> Self {
Point { x: 0.0, y: 0.0 }
}
fn distance(&self, other: &Point) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
}
// Approach 2: Struct update syntax
#[derive(Debug, Clone)]
struct Config {
host: String,
port: u16,
debug: bool,
timeout: u32,
}
impl Config {
fn default_config() -> Self {
Config {
host: "localhost".to_string(),
port: 8080,
debug: false,
timeout: 30,
}
}
}
fn dev_config() -> Config {
Config {
debug: true,
port: 3000,
..Config::default_config()
}
}
fn prod_config() -> Config {
Config {
host: "prod.example.com".to_string(),
timeout: 60,
..Config::default_config()
}
}
// Approach 3: Destructuring
fn describe_config(config: &Config) -> String {
let Config {
host, port, debug, ..
} = config;
format!("{}:{}{}", host, port, if *debug { " [DEBUG]" } else { "" })
}
fn is_local(config: &Config) -> bool {
config.host == "localhost" || config.host == "127.0.0.1"
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_point() {
let o = Point::origin();
let p = Point { x: 3.0, y: 4.0 };
assert!((o.distance(&p) - 5.0).abs() < 0.001);
}
#[test]
fn test_struct_update() {
let dev = dev_config();
assert!(dev.debug);
assert_eq!(dev.port, 3000);
assert_eq!(dev.host, "localhost");
}
#[test]
fn test_prod_config() {
let prod = prod_config();
assert_eq!(prod.timeout, 60);
assert_eq!(prod.host, "prod.example.com");
}
#[test]
fn test_describe() {
assert_eq!(describe_config(&dev_config()), "localhost:3000 [DEBUG]");
}
#[test]
fn test_is_local() {
assert!(is_local(&Config::default_config()));
assert!(!is_local(&prod_config()));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_point() {
let o = Point::origin();
let p = Point { x: 3.0, y: 4.0 };
assert!((o.distance(&p) - 5.0).abs() < 0.001);
}
#[test]
fn test_struct_update() {
let dev = dev_config();
assert!(dev.debug);
assert_eq!(dev.port, 3000);
assert_eq!(dev.host, "localhost");
}
#[test]
fn test_prod_config() {
let prod = prod_config();
assert_eq!(prod.timeout, 60);
assert_eq!(prod.host, "prod.example.com");
}
#[test]
fn test_describe() {
assert_eq!(describe_config(&dev_config()), "localhost:3000 [DEBUG]");
}
#[test]
fn test_is_local() {
assert!(is_local(&Config::default_config()));
assert!(!is_local(&prod_config()));
}
}
Deep Comparison
Core Insight
Records/structs group named fields. Both languages support pattern matching on fields and functional update syntax (creating a new value with some fields changed).
OCaml Approach
type t = { field1: type1; field2: type2 } — record definition{ r with field = new_value } — functional updatemutable keyword (rare)let { field1; field2 } = rRust Approach
struct T { field1: Type1, field2: Type2 } — struct definitionT { field: new_val, ..old } — struct update syntax (moves non-Copy fields!)pub for visibilitylet T { field1, field2 } = s;Comparison Table
| Feature | OCaml | Rust |
|---|---|---|
| Define | type t = { x: int } | struct T { x: i32 } |
| Create | { x = 5 } | T { x: 5 } |
| Access | r.x | s.x |
| Update | { r with x = 10 } | T { x: 10, ..s } |
| Destructure | let { x; y } = r | let T { x, y } = s |
| Mutability | mutable per field | let mut s (all or nothing) |
Exercises
ConfigBuilder struct with setter methods that each return Self (for chaining) and a build() -> Config method. This is idiomatic Rust for structs with many optional fields.#[derive(serde::Serialize, serde::Deserialize)] to Config and serialize/deserialize to/from JSON using serde_json.Default for Config using #[derive(Default)] (set all fields to their defaults) or a manual impl Default. Compare with the manual default_config() function.Config struct with many optional fields, using the builder pattern (a struct ConfigBuilder with a chain of setters and a final .build() -> Result<Config, String>).Point struct, then compose them to update nested fields — introducing the concept of lenses without a dedicated library.