Lens Laws
Tutorial
The Problem
A "lens" that violates basic consistency properties is useless — if set does not set or get does not get what was set, compositions break silently. Three laws define what it means for a lens to be "lawful": GetSet (setting then getting gives back what you set), SetGet (getting then setting the same value is a no-op), and SetSet (setting twice is the same as setting once). Law-checking tests catch implementation bugs early.
🎯 Learning Outcomes
Code Example
fn check_get_set<S: PartialEq + Clone, A: Clone>(lens: &Lens<S, A>, s: &S) -> bool {
let a = (lens.get)(s);
(lens.set)(a, s) == *s
}
fn check_set_get<S: Clone, A: PartialEq + Clone>(lens: &Lens<S, A>, a: A, s: &S) -> bool {
(lens.get)(&(lens.set)(a.clone(), s)) == a
}
fn check_set_set<S: PartialEq + Clone, A: Clone>(lens: &Lens<S, A>, a: A, b: A, s: &S) -> bool {
(lens.set)(b.clone(), &(lens.set)(a, s)) == (lens.set)(b, s)
}Key Differences
proptest/quickcheck, OCaml: QCheck) to verify lens laws over many random inputs.OCaml Approach
OCaml's optics libraries ship with property-based tests using QCheck. The laws are phrased identically — they are mathematical properties independent of the programming language. OCaml's ppx_lens-generated lenses are always lawful by construction (they directly access record fields). Hand-written lenses require manual law verification.
Full Source
#![allow(clippy::all)]
// Example 203: Lens Laws — GetSet, SetGet, SetSet
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
struct Lens<S, A> {
get: Box<dyn Fn(&S) -> A>,
set: Box<dyn Fn(A, &S) -> S>,
}
impl<S, A> 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),
}
}
}
// Approach 1: Lawful lenses
fn x_lens() -> Lens<Point, f64> {
Lens::new(
|p: &Point| p.x,
|x: f64, p: &Point| Point { x, ..p.clone() },
)
}
fn y_lens() -> Lens<Point, f64> {
Lens::new(
|p: &Point| p.y,
|y: f64, p: &Point| Point { y, ..p.clone() },
)
}
// Approach 2: An UNLAWFUL lens — set has a side effect
fn bad_lens() -> Lens<Point, f64> {
Lens::new(
|p: &Point| p.x,
|x: f64, p: &Point| Point { x, y: p.y + 1.0 }, // mutates y!
)
}
// Approach 3: Law verification
fn check_get_set<S: PartialEq + Clone, A: Clone>(lens: &Lens<S, A>, s: &S) -> bool {
let a = (lens.get)(s);
let result = (lens.set)(a, s);
result == *s
}
fn check_set_get<S: Clone, A: PartialEq + Clone>(lens: &Lens<S, A>, a: A, s: &S) -> bool {
let result = (lens.get)(&(lens.set)(a.clone(), s));
result == a
}
fn check_set_set<S: PartialEq + Clone, A: Clone>(lens: &Lens<S, A>, a: A, b: A, s: &S) -> bool {
let r1 = (lens.set)(b.clone(), &(lens.set)(a, s));
let r2 = (lens.set)(b, s);
r1 == r2
}
fn verify_laws<S: PartialEq + Clone, A: PartialEq + Clone>(
name: &str,
lens: &Lens<S, A>,
s: &S,
a: A,
b: A,
) -> (bool, bool, bool) {
let gs = check_get_set(lens, s);
let sg = check_set_get(lens, a.clone(), s);
let ss = check_set_set(lens, a, b, s);
println!("{}: GetSet={} SetGet={} SetSet={}", name, gs, sg, ss);
(gs, sg, ss)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_x_lens_lawful() {
let p = Point { x: 1.0, y: 2.0 };
let l = x_lens();
assert!(check_get_set(&l, &p));
assert!(check_set_get(&l, 99.0, &p));
assert!(check_set_set(&l, 5.0, 10.0, &p));
}
#[test]
fn test_bad_lens_unlawful() {
let p = Point { x: 1.0, y: 2.0 };
let l = bad_lens();
assert!(!check_get_set(&l, &p));
}
#[test]
fn test_set_get_law() {
let p = Point { x: 0.0, y: 0.0 };
let l = y_lens();
let result = (l.get)(&(l.set)(42.0, &p));
assert_eq!(result, 42.0);
}
#[test]
fn test_set_set_law() {
let p = Point { x: 1.0, y: 2.0 };
let l = x_lens();
let r1 = (l.set)(99.0, &(l.set)(50.0, &p));
let r2 = (l.set)(99.0, &p);
assert_eq!(r1, r2);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_x_lens_lawful() {
let p = Point { x: 1.0, y: 2.0 };
let l = x_lens();
assert!(check_get_set(&l, &p));
assert!(check_set_get(&l, 99.0, &p));
assert!(check_set_set(&l, 5.0, 10.0, &p));
}
#[test]
fn test_bad_lens_unlawful() {
let p = Point { x: 1.0, y: 2.0 };
let l = bad_lens();
assert!(!check_get_set(&l, &p));
}
#[test]
fn test_set_get_law() {
let p = Point { x: 0.0, y: 0.0 };
let l = y_lens();
let result = (l.get)(&(l.set)(42.0, &p));
assert_eq!(result, 42.0);
}
#[test]
fn test_set_set_law() {
let p = Point { x: 1.0, y: 2.0 };
let l = x_lens();
let r1 = (l.set)(99.0, &(l.set)(50.0, &p));
let r2 = (l.set)(99.0, &p);
assert_eq!(r1, r2);
}
}
Deep Comparison
Comparison: Example 203 — Lens Laws
Law Definitions
OCaml
(* GetSet: set (get s) s = s *)
let check_get_set lens s =
lens.set (lens.get s) s = s
(* SetGet: get (set a s) = a *)
let check_set_get lens a s =
lens.get (lens.set a s) = a
(* SetSet: set b (set a s) = set b s *)
let check_set_set lens a b s =
lens.set b (lens.set a s) = lens.set b s
Rust
fn check_get_set<S: PartialEq + Clone, A: Clone>(lens: &Lens<S, A>, s: &S) -> bool {
let a = (lens.get)(s);
(lens.set)(a, s) == *s
}
fn check_set_get<S: Clone, A: PartialEq + Clone>(lens: &Lens<S, A>, a: A, s: &S) -> bool {
(lens.get)(&(lens.set)(a.clone(), s)) == a
}
fn check_set_set<S: PartialEq + Clone, A: Clone>(lens: &Lens<S, A>, a: A, b: A, s: &S) -> bool {
(lens.set)(b.clone(), &(lens.set)(a, s)) == (lens.set)(b, s)
}
Unlawful Lens
OCaml
let bad_lens = {
get = (fun p -> p.x);
set = (fun x p -> { x; y = p.y +. 1.0 }); (* side effect! *)
}
Rust
fn bad_lens() -> Lens<Point, f64> {
Lens::new(|p| p.x, |x, p| Point { x, y: p.y + 1.0 }) // side effect!
}
Exercises
get, and show which law it violates.#[cfg(test)] property-based tests using quickcheck that verify all three laws for the Point.x lens.