// Example 201: The Nested Update Problem — Why Lenses Exist
// === The Problem: Deeply Nested Struct Updates === //
#[derive(Debug, Clone, PartialEq)]
struct DbConfig {
host: String,
port: u16,
name: String,
}
#[derive(Debug, Clone, PartialEq)]
struct ServerConfig {
db: DbConfig,
max_connections: u32,
}
#[derive(Debug, Clone, PartialEq)]
struct AppConfig {
server: ServerConfig,
debug: bool,
version: String,
}
// Approach 1: Manual nested update — clone everything by hand
fn update_db_port_manual(config: &AppConfig, new_port: u16) -> AppConfig {
AppConfig {
server: ServerConfig {
db: DbConfig {
port: new_port,
..config.server.db.clone()
},
..config.server.clone()
},
..config.clone()
}
}
// Approach 2: Helper functions — map at each level
fn map_server(f: impl FnOnce(ServerConfig) -> ServerConfig, config: &AppConfig) -> AppConfig {
AppConfig {
server: f(config.server.clone()),
..config.clone()
}
}
fn map_db(f: impl FnOnce(DbConfig) -> DbConfig, server: ServerConfig) -> ServerConfig {
ServerConfig {
db: f(server.db.clone()),
..server
}
}
fn set_port(port: u16, db: DbConfig) -> DbConfig {
DbConfig { port, ..db }
}
fn update_db_port_helpers(config: &AppConfig, new_port: u16) -> AppConfig {
map_server(|s| map_db(|d| set_port(new_port, d), s), config)
}
// Approach 3: Lenses — composable getters and setters
struct Lens<S, A> {
get: Box<dyn Fn(&S) -> A>,
set: Box<dyn Fn(A, &S) -> S>,
}
impl<S: 'static, A: 'static> Lens<S, A> {
fn new(
get: impl Fn(&S) -> A + 'static,
set: impl Fn(A, &S) -> S + 'static,
) -> Self {
Lens {
get: Box::new(get),
set: Box::new(set),
}
}
fn compose<B: 'static>(self, inner: Lens<A, B>) -> Lens<S, B>
where
A: Clone,
S: Clone,
{
use std::rc::Rc;
let outer_get: Rc<dyn Fn(&S) -> A> = Rc::from(self.get);
let outer_set = self.set;
let inner_get = inner.get;
let inner_set = inner.set;
let og1 = outer_get.clone();
let og2 = outer_get;
Lens {
get: Box::new(move |s| (inner_get)(&(og1)(s))),
set: Box::new(move |b, s| {
let a = (og2)(s);
let new_a = (inner_set)(b, &a);
(outer_set)(new_a, s)
}),
}
}
}
fn server_lens() -> Lens<AppConfig, ServerConfig> {
Lens::new(
|c: &AppConfig| c.server.clone(),
|s: ServerConfig, c: &AppConfig| AppConfig { server: s, ..c.clone() },
)
}
fn db_lens() -> Lens<ServerConfig, DbConfig> {
Lens::new(
|s: &ServerConfig| s.db.clone(),
|d: DbConfig, s: &ServerConfig| ServerConfig { db: d, ..s.clone() },
)
}
fn port_lens() -> Lens<DbConfig, u16> {
Lens::new(
|d: &DbConfig| d.port,
|p: u16, d: &DbConfig| DbConfig { port: p, ..d.clone() },
)
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_config() -> AppConfig {
AppConfig {
server: ServerConfig {
db: DbConfig {
host: "localhost".into(),
port: 5432,
name: "mydb".into(),
},
max_connections: 100,
},
debug: false,
version: "1.0".into(),
}
}
#[test]
fn test_manual_update() {
let c = update_db_port_manual(&sample_config(), 5433);
assert_eq!(c.server.db.port, 5433);
assert_eq!(c.server.max_connections, 100);
}
#[test]
fn test_helper_update() {
let c = update_db_port_helpers(&sample_config(), 5433);
assert_eq!(c.server.db.port, 5433);
}
#[test]
fn test_lens_update() {
let lens = server_lens().compose(db_lens()).compose(port_lens());
let c = (lens.set)(5433, &sample_config());
assert_eq!((lens.get)(&c), 5433);
assert_eq!(c.server.max_connections, 100);
}
#[test]
fn test_all_equivalent() {
let cfg = sample_config();
let c1 = update_db_port_manual(&cfg, 9999);
let c2 = update_db_port_helpers(&cfg, 9999);
let lens = server_lens().compose(db_lens()).compose(port_lens());
let c3 = (lens.set)(9999, &cfg);
assert_eq!(c1, c2);
assert_eq!(c2, c3);
}
}
(* Example 201: The Nested Update Problem — Why Lenses Exist *)
(* === The Problem: Deeply Nested Record Updates === *)
type db_config = {
host : string;
port : int;
name : string;
}
type server_config = {
db : db_config;
max_connections : int;
}
type app_config = {
server : server_config;
debug : bool;
version : string;
}
(* Approach 1: Manual nested update — the pain *)
let update_db_port_manual config new_port =
{ config with
server = { config.server with
db = { config.server.db with
port = new_port
}
}
}
(* Look at that nesting! And it gets worse with deeper structures. *)
(* Approach 2: Helper functions for each level *)
let map_server f config = { config with server = f config.server }
let map_db f server = { server with db = f server.db }
let set_port port db = { db with port }
let update_db_port_helpers config new_port =
config |> map_server (map_db (set_port new_port))
(* Better! But we need a helper for every field at every level. *)
(* Approach 3: Lenses — composable getters and setters *)
type ('s, 'a) lens = {
get : 's -> 'a;
set : 'a -> 's -> 's;
}
let compose outer inner = {
get = (fun s -> inner.get (outer.get s));
set = (fun a s -> outer.set (inner.set a (outer.get s)) s);
}
let server_lens = {
get = (fun c -> c.server);
set = (fun s c -> { c with server = s });
}
let db_lens = {
get = (fun s -> s.db);
set = (fun d s -> { s with db = d });
}
let port_lens = {
get = (fun d -> d.port);
set = (fun p d -> { d with port = p });
}
(* Compose to zoom all the way in *)
let app_db_port = compose (compose server_lens db_lens) port_lens
let update_db_port_lens config new_port =
app_db_port.set new_port config
(* Now ANY depth is just composition! *)
(* === Tests === *)
let () =
let config = {
server = {
db = { host = "localhost"; port = 5432; name = "mydb" };
max_connections = 100;
};
debug = false;
version = "1.0";
} in
(* Test manual *)
let c1 = update_db_port_manual config 5433 in
assert (c1.server.db.port = 5433);
assert (c1.server.max_connections = 100);
assert (c1.debug = false);
(* Test helpers *)
let c2 = update_db_port_helpers config 5433 in
assert (c2.server.db.port = 5433);
(* Test lens *)
let c3 = update_db_port_lens config 5433 in
assert (c3.server.db.port = 5433);
assert (app_db_port.get c3 = 5433);
(* All three produce same result *)
assert (c1 = c2);
assert (c2 = c3);
print_endline "✓ All tests passed"