🦀 Functional Rust

617: Prism Pattern — Fallible Access for Enum Variants

Difficulty: 5 Level: Master A Prism is the enum counterpart to a Lens. Where a Lens always succeeds (every struct has the field), a Prism may fail (not every enum is the right variant).

The Problem This Solves

You have an enum — say, a JSON type with six variants: `Null`, `Bool`, `Num`, `Str`, `Arr`, `Obj`. You want to write generic code that processes "the boolean value, if any" from a list of JSON values. Without Prisms, you write `match` at every call site:
let bools: Vec<bool> = jsons.iter().filter_map(|j| {
 match j {
     Json::Bool(b) => Some(*b),
     _ => None,
 }
}).collect();
That's fine. But now imagine the same pattern for every variant, in every function, across a codebase with ten enum types. The `match` arms are repeated, the "extract if this variant" logic is scattered, and adding a new variant forces you to review every `match`. A Prism makes variant access first-class. You define the extraction logic once (in the Prism's `preview` function) and the construction logic once (in `review`). Then pass the Prism as a value — to filtering functions, mapping functions, or any generic code that works with "some variant of some type." The Prism is your typed, named, reusable accessor for that one variant. This example exists to solve exactly that pain.

The Intuition

A Lens has two operations: `get` (always returns `A`) and `set` (always succeeds). A Prism has two operations: `preview` is like a type-safe checked downcast. Given a `Json`, is it a `Bool`? If yes, give me the `bool`. If not, give me `None`. `review` is the variant constructor. Given a `bool`, make a `Json::Bool(b)`. The two operations are inverses when the variant matches: These are the Prism laws (see example 207). They ensure the Prism makes sense — that `preview` and `review` are talking about the same variant in the same way. `over` — conditional transformation: Unlike a Lens (where `over` always applies), a Prism's `over` only transforms when `preview` succeeds:
fn over(&self, f: impl Fn(A) -> A, s: S) -> S {
 match self.preview(&s) {
     Some(a) => self.review(f(a)),  // matching variant: transform it
     None    => s,                  // wrong variant: return unchanged
 }
}

How It Works in Rust

Define Prisms for your enum:
let bool_prism: Prism<Json, bool> = Prism::new(
 |j| match j { Json::Bool(b) => Some(*b), _ => None },
 |b| Json::Bool(b),
);

let num_prism: Prism<Json, f64> = Prism::new(
 |j| match j { Json::Num(n) => Some(*n), _ => None },
 |n| Json::Num(n),
);
Verify the laws built into the Prism:
// Law 1: preview(review(a)) == Some(a)
bool_prism.law_preview_review(true)   // true — it's lawful

// Law 2: if preview(s) == Some(a) then review(a) == s
bool_prism.law_review_preview(&Json::Num(1.0))  // true — Num doesn't match, vacuously true
`over` — transform only matching variants:
let j1 = Json::Bool(false);
let j2 = bool_prism.over(|b| !b, j1);   // Json::Bool(true)

let j3 = Json::Null;
let j4 = bool_prism.over(|b| !b, j3);   // Json::Null — unchanged, Null isn't a Bool
Filter a collection using `preview`:
let jsons = vec![Json::Bool(true), Json::Num(3.14), Json::Str("hi".into()), Json::Null];

let bools: Vec<bool> = jsons.iter()
 .filter_map(|j| bool_prism.preview(j))
 .collect();
// [true] — only the Bool variant matched
The Prism replaces the inline `match` with a named, reusable accessor. Pass `bool_prism` as a value to any function that needs to extract booleans from JSON.

What This Unlocks

Key Differences

ConceptOCamlRust
`preview` signature`'s -> 'a option``fn(&S) -> Option<A>` — identical concept
`review` signature`'a -> 's``fn(A) -> S` — identical concept
Law: ReviewPreview`preview(review(a)) = Some(a)``self.preview(&self.review(a.clone())) == Some(a)`
Law: PreviewReviewIf `preview(s)=Some(a)` then `review(a)=s`Same — `match self.preview(s)` with clone
`over` for non-matchReturns `s` unchangedReturns `s` unchanged — requires `S: Clone` if by reference
Composition`prism1.compose(prism2)`Manual — chain `preview` calls with `and_then`
// Prism: fallible optic for sum types
struct Prism<S, A> {
    preview_fn: Box<dyn Fn(&S) -> Option<A>>,
    review_fn:  Box<dyn Fn(A) -> S>,
}

impl<S: Clone + PartialEq, A: Clone + PartialEq> Prism<S, A> {
    fn new(preview_fn: impl Fn(&S)->Option<A>+'static, review_fn: impl Fn(A)->S+'static) -> Self {
        Prism { preview_fn: Box::new(preview_fn), review_fn: Box::new(review_fn) }
    }
    fn preview(&self, s: &S) -> Option<A> { (self.preview_fn)(s) }
    fn review(&self, a: A) -> S { (self.review_fn)(a) }
    fn over(&self, f: impl Fn(A)->A, s: S) -> S {
        match self.preview(&s) {
            Some(a) => self.review(f(a)),
            None    => s,
        }
    }

    // Laws
    fn law_preview_review(&self, a: A) -> bool where A: std::fmt::Debug {
        self.preview(&self.review(a.clone())) == Some(a)
    }
    fn law_review_preview(&self, s: &S) -> bool {
        match self.preview(s) {
            None    => true,
            Some(a) => &self.review(a) == s,
        }
    }
}

// JSON-like type for demonstration
#[derive(Debug,Clone,PartialEq)]
enum Json { Null, Bool(bool), Num(f64), Str(String), Arr(Vec<Json>) }

fn main() {
    let bool_prism: Prism<Json,bool> = Prism::new(
        |j| match j { Json::Bool(b) => Some(*b), _ => None },
        |b| Json::Bool(b),
    );
    let num_prism: Prism<Json,f64> = Prism::new(
        |j| match j { Json::Num(n) => Some(*n), _ => None },
        |n| Json::Num(n),
    );

    // Laws
    println!("bool law preview∘review(true):  {}", bool_prism.law_preview_review(true));
    println!("bool law review∘preview(Num):   {}", bool_prism.law_review_preview(&Json::Num(1.0)));
    println!("num  law preview∘review(42.0):  {}", num_prism.law_preview_review(42.0));

    // over: modify if matches
    let j1 = Json::Bool(false);
    let j2 = bool_prism.over(|b| !b, j1);
    println!("!false = {:?}", j2);

    let j3 = Json::Null;
    let j4 = bool_prism.over(|b| !b, j3.clone());
    println!("over Null = {:?} (unchanged)", j4);

    // Compose prisms manually (via preview/review)
    let jsons = vec![Json::Bool(true), Json::Num(3.14), Json::Str("hi".into()), Json::Null];
    let bools: Vec<bool> = jsons.iter().filter_map(|j| bool_prism.preview(j)).collect();
    println!("bools from json array: {:?}", bools);
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test] fn prism_law_bool() {
        let p: Prism<Json,bool> = Prism::new(
            |j| match j { Json::Bool(b)=>Some(*b), _=>None }, Json::Bool);
        assert!(p.law_preview_review(true));
        assert!(p.law_review_preview(&Json::Num(1.0)));
    }
}
(* Prism in OCaml *)
type ('s,'a) prism = {
  preview: 's -> 'a option;
  review:  'a -> 's;
}

(* Prisms for a JSON-like type *)
type json = Null | JBool of bool | JNum of float | JStr of string | JArr of json list

let json_bool = {
  preview = (function JBool b -> Some b | _ -> None);
  review  = (fun b -> JBool b);
}

let json_num = {
  preview = (function JNum n -> Some n | _ -> None);
  review  = (fun n -> JNum n);
}

let json_str = {
  preview = (function JStr s -> Some s | _ -> None);
  review  = (fun s -> JStr s);
}

(* Prism law checks *)
let preview_review p a = p.preview (p.review a) = Some a
let review_preview_consistency p s = match p.preview s with
  | None   -> true
  | Some a -> p.review a = s

let () =
  Printf.printf "law review_preview JBool: %b\n" (preview_review json_bool true);
  Printf.printf "law review_preview JNum:  %b\n" (preview_review json_num 42.0);
  Printf.printf "preview JBool on JNum: %s\n"
    (match json_bool.preview (JNum 1.0) with None->"None" | Some b->string_of_bool b)