ExamplesBy LevelBy TopicLearning Paths
207 Advanced

Prism Laws

Functional Programming

Tutorial

The Problem

A prism that violates its consistency laws — where review followed by preview does not give back the original value, or where preview succeeds but review of the result does not reconstruct the original — is an unreliable abstraction that breaks compositions silently. The two prism laws (ReviewPreview and PreviewReview) define what it means for a prism to be lawful and trustworthy, just as the three lens laws define a trustworthy lens.

🎯 Learning Outcomes

  • • Understand the two prism laws: ReviewPreview and PreviewReview
  • • Learn how prism laws guarantee round-trip consistency between preview and review
  • • See examples of lawful and unlawful prisms
  • • Write tests that verify prism law compliance
  • Code Example

    pub struct Prism<S, A> {
        preview: Box<dyn Fn(&S) -> Option<A>>,
        review:  Box<dyn Fn(A) -> S>,
    }
    
    /// Law 1 — ReviewPreview: preview(review(a)) == Some(a)
    pub fn check_review_preview<S, A>(prism: &Prism<S, A>, a: A) -> bool
    where
        A: PartialEq + Clone + 'static,
        S: 'static,
    {
        let s = prism.review(a.clone());
        prism.preview(&s) == Some(a)
    }
    
    /// Law 2 — PreviewReview: if preview(s) == Some(a) then review(a) == s
    pub fn check_preview_review<S, A>(prism: &Prism<S, A>, s: &S) -> bool
    where
        S: PartialEq + 'static,
        A: 'static,
    {
        match prism.preview(s) {
            None    => true,
            Some(a) => prism.review(a) == *s,
        }
    }
    
    pub fn jstring_prism() -> Prism<Json, String> {
        Prism::new(
            |j| match j { Json::JString(s) => Some(s.clone()), _ => None },
            Json::JString,
        )
    }
    
    pub fn unlawful_uppercase_prism() -> Prism<Json, String> {
        Prism::new(
            |j| match j { Json::JString(s) => Some(s.to_uppercase()), _ => None },
            Json::JString,
        )
    }

    Key Differences

  • Two laws vs. three: Prisms have two laws; lenses have three — fewer laws because prisms lack the "SetSet" idempotency requirement.
  • Law strength: ReviewPreview is the strongest constraint — it forces review to be injective (different a values produce different s values).
  • Property testing: Both languages benefit from property-based testing over many random inputs to verify laws; a single test case is insufficient.
  • Lawful by construction: Prisms generated from exact variant pattern matches are always lawful; prisms with transformations in review are suspect.
  • OCaml Approach

    OCaml's prism laws are expressed identically — mathematical properties. Law-checking in OCaml uses QCheck for property-based testing:

    QCheck.Test.make QCheck.int (fun a ->
      prism.preview (prism.review a) = Some a)
    

    OCaml's pattern matching makes preview implementations more naturally lawful than Rust's closure-based approach, reducing the risk of accidentally violating laws.

    Full Source

    #![allow(clippy::all)]
    // Example 207: Prism Laws — ReviewPreview and PreviewReview
    //
    // Two round-trip laws guarantee that a Prism's `preview` and `review` are
    // consistent with each other:
    //
    //   Law 1 — ReviewPreview:  preview(review(a)) = Some(a)
    //     "If I build an S with review, I can always get back the exact a I used."
    //
    //   Law 2 — PreviewReview:  if preview(s) = Some(a) then review(a) = s
    //     "If extraction succeeds, re-injection gives back the original value."
    //
    // A Prism that violates either law compiles and passes naive tests but breaks
    // silently when composed.  These law-checkers let you catch violations at
    // test time.
    
    // ---------------------------------------------------------------------------
    // Core Prism struct
    // ---------------------------------------------------------------------------
    
    type PreviewFn<S, A> = Box<dyn Fn(&S) -> Option<A>>;
    type ReviewFn<S, A> = Box<dyn Fn(A) -> S>;
    
    /// A Prism: two functions (`preview` and `review`) that must satisfy the
    /// ReviewPreview and PreviewReview laws to be well-behaved.
    pub struct Prism<S, A> {
        preview: PreviewFn<S, A>,
        review: ReviewFn<S, A>,
    }
    
    impl<S: 'static, A: 'static> Prism<S, A> {
        pub fn new(
            preview: impl Fn(&S) -> Option<A> + 'static,
            review: impl Fn(A) -> S + 'static,
        ) -> Self {
            Prism {
                preview: Box::new(preview),
                review: Box::new(review),
            }
        }
    
        pub fn preview(&self, s: &S) -> Option<A> {
            (self.preview)(s)
        }
    
        pub fn review(&self, a: A) -> S {
            (self.review)(a)
        }
    }
    
    // ---------------------------------------------------------------------------
    // Law checkers
    // ---------------------------------------------------------------------------
    
    /// **Law 1 — ReviewPreview**: `preview(review(a)) == Some(a)`
    ///
    /// Build an `S` from `a`, then try to extract it back.  A lawful Prism must
    /// always succeed and return exactly the `a` we started with.
    pub fn check_review_preview<S, A>(prism: &Prism<S, A>, a: A) -> bool
    where
        A: PartialEq + Clone + 'static,
        S: 'static,
    {
        let s = prism.review(a.clone());
        prism.preview(&s) == Some(a)
    }
    
    /// **Law 2 — PreviewReview**: if `preview(s) == Some(a)` then `review(a) == s`
    ///
    /// If we can extract an `a` from `s`, then re-injecting it must reproduce `s`
    /// exactly.  Returns `true` when the precondition fails (the law is vacuously
    /// satisfied when `preview` returns `None`).
    pub fn check_preview_review<S, A>(prism: &Prism<S, A>, s: &S) -> bool
    where
        S: PartialEq + 'static,
        A: 'static,
    {
        match prism.preview(s) {
            None => true, // vacuously lawful — precondition not met
            Some(a) => prism.review(a) == *s,
        }
    }
    
    // ---------------------------------------------------------------------------
    // Domain model: a simple JSON type
    // ---------------------------------------------------------------------------
    
    #[derive(Debug, Clone, PartialEq)]
    pub enum Json {
        JString(String),
        JInt(i64),
        JBool(bool),
        JNull,
        JArray(Vec<Json>),
    }
    
    // ---------------------------------------------------------------------------
    // Approach 1: Lawful prisms (both laws hold)
    // ---------------------------------------------------------------------------
    
    /// Prism focusing on `Json::JString`. Lawful: round-trips are exact.
    pub fn jstring_prism() -> Prism<Json, String> {
        Prism::new(
            |j| match j {
                Json::JString(s) => Some(s.clone()),
                _ => None,
            },
            Json::JString,
        )
    }
    
    /// Prism focusing on `Json::JInt`. Lawful.
    pub fn jint_prism() -> Prism<Json, i64> {
        Prism::new(
            |j| match j {
                Json::JInt(n) => Some(*n),
                _ => None,
            },
            Json::JInt,
        )
    }
    
    /// Prism focusing on `Json::JBool`. Lawful.
    pub fn jbool_prism() -> Prism<Json, bool> {
        Prism::new(
            |j| match j {
                Json::JBool(b) => Some(*b),
                _ => None,
            },
            Json::JBool,
        )
    }
    
    // ---------------------------------------------------------------------------
    // Approach 2: Unlawful prism — demonstrates what law violation looks like
    // ---------------------------------------------------------------------------
    
    /// An **unlawful** prism: `preview` uppercases the string it returns, but
    /// `review` stores the original case.  This violates Law 1 (ReviewPreview):
    ///
    /// ```text
    /// review("hello") -> JString("hello")
    /// preview(JString("hello")) -> Some("HELLO")   ← not Some("hello")!
    /// ```
    ///
    /// Code compiles.  Basic usage looks fine.  But round-trips corrupt data.
    pub fn unlawful_uppercase_prism() -> Prism<Json, String> {
        Prism::new(
            |j| match j {
                // BUG: transforms the value during extraction
                Json::JString(s) => Some(s.to_uppercase()),
                _ => None,
            },
            Json::JString,
        )
    }
    
    // ---------------------------------------------------------------------------
    // Approach 3: Trait-based law verification (compile-time dispatch)
    // ---------------------------------------------------------------------------
    
    /// Implement this trait to get zero-cost Prism dispatch with built-in law checks.
    pub trait LawfulPrism {
        type Source: Clone + PartialEq;
        type Focus: Clone + PartialEq + 'static;
    
        fn preview(s: &Self::Source) -> Option<Self::Focus>;
        fn review(a: Self::Focus) -> Self::Source;
    
        /// Checks Law 1 for a given focus value.
        fn law_review_preview(a: Self::Focus) -> bool {
            let s = Self::review(a.clone());
            Self::preview(&s) == Some(a)
        }
    
        /// Checks Law 2 for a given source value.
        fn law_preview_review(s: &Self::Source) -> bool {
            match Self::preview(s) {
                None => true,
                Some(a) => Self::review(a) == *s,
            }
        }
    }
    
    /// Zero-cost lawful prism for `Json::JString` via the trait approach.
    pub struct JStringPrism;
    
    impl LawfulPrism for JStringPrism {
        type Source = Json;
        type Focus = String;
    
        fn preview(j: &Json) -> Option<String> {
            match j {
                Json::JString(s) => Some(s.clone()),
                _ => None,
            }
        }
    
        fn review(s: String) -> Json {
            Json::JString(s)
        }
    }
    
    // ---------------------------------------------------------------------------
    // Tests
    // ---------------------------------------------------------------------------
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // --- Law 1: ReviewPreview ---
    
        #[test]
        fn test_law1_jstring_review_then_preview_returns_original() {
            let p = jstring_prism();
            assert!(check_review_preview(&p, "hello".to_string()));
        }
    
        #[test]
        fn test_law1_jint_review_then_preview_returns_original() {
            let p = jint_prism();
            assert!(check_review_preview(&p, 42_i64));
        }
    
        #[test]
        fn test_law1_jbool_review_then_preview_returns_original() {
            let p = jbool_prism();
            assert!(check_review_preview(&p, true));
            assert!(check_review_preview(&p, false));
        }
    
        #[test]
        fn test_law1_unlawful_prism_violates_review_preview() {
            // The unlawful prism transforms during preview, so Law 1 must fail.
            let p = unlawful_uppercase_prism();
            assert!(!check_review_preview(&p, "hello".to_string()));
        }
    
        // --- Law 2: PreviewReview ---
    
        #[test]
        fn test_law2_jstring_preview_then_review_gives_original() {
            let p = jstring_prism();
            let s = Json::JString("world".to_string());
            assert!(check_preview_review(&p, &s));
        }
    
        #[test]
        fn test_law2_jstring_wrong_variant_vacuously_true() {
            // preview returns None for JInt — law is vacuously satisfied.
            let p = jstring_prism();
            let s = Json::JInt(99);
            assert!(check_preview_review(&p, &s));
        }
    
        #[test]
        fn test_law2_jint_preview_then_review_gives_original() {
            let p = jint_prism();
            let s = Json::JInt(-7);
            assert!(check_preview_review(&p, &s));
        }
    
        #[test]
        fn test_law2_jnull_vacuously_true_for_jstring_prism() {
            let p = jstring_prism();
            assert!(check_preview_review(&p, &Json::JNull));
        }
    
        // --- Trait-based law checks ---
    
        #[test]
        fn test_trait_prism_law1_jstring() {
            assert!(JStringPrism::law_review_preview("rust".to_string()));
        }
    
        #[test]
        fn test_trait_prism_law2_jstring_matching_variant() {
            let s = Json::JString("optics".to_string());
            assert!(JStringPrism::law_preview_review(&s));
        }
    
        #[test]
        fn test_trait_prism_law2_jstring_non_matching_variant() {
            assert!(JStringPrism::law_preview_review(&Json::JBool(true)));
        }
    
        // --- Concrete behavior ---
    
        #[test]
        fn test_jstring_prism_preview_matching() {
            let p = jstring_prism();
            let j = Json::JString("abc".to_string());
            assert_eq!(p.preview(&j), Some("abc".to_string()));
        }
    
        #[test]
        fn test_jstring_prism_preview_non_matching() {
            let p = jstring_prism();
            assert_eq!(p.preview(&Json::JNull), None);
            assert_eq!(p.preview(&Json::JInt(1)), None);
        }
    
        #[test]
        fn test_jint_prism_review_then_preview() {
            let p = jint_prism();
            let constructed = p.review(100);
            assert_eq!(constructed, Json::JInt(100));
            assert_eq!(p.preview(&constructed), Some(100));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // --- Law 1: ReviewPreview ---
    
        #[test]
        fn test_law1_jstring_review_then_preview_returns_original() {
            let p = jstring_prism();
            assert!(check_review_preview(&p, "hello".to_string()));
        }
    
        #[test]
        fn test_law1_jint_review_then_preview_returns_original() {
            let p = jint_prism();
            assert!(check_review_preview(&p, 42_i64));
        }
    
        #[test]
        fn test_law1_jbool_review_then_preview_returns_original() {
            let p = jbool_prism();
            assert!(check_review_preview(&p, true));
            assert!(check_review_preview(&p, false));
        }
    
        #[test]
        fn test_law1_unlawful_prism_violates_review_preview() {
            // The unlawful prism transforms during preview, so Law 1 must fail.
            let p = unlawful_uppercase_prism();
            assert!(!check_review_preview(&p, "hello".to_string()));
        }
    
        // --- Law 2: PreviewReview ---
    
        #[test]
        fn test_law2_jstring_preview_then_review_gives_original() {
            let p = jstring_prism();
            let s = Json::JString("world".to_string());
            assert!(check_preview_review(&p, &s));
        }
    
        #[test]
        fn test_law2_jstring_wrong_variant_vacuously_true() {
            // preview returns None for JInt — law is vacuously satisfied.
            let p = jstring_prism();
            let s = Json::JInt(99);
            assert!(check_preview_review(&p, &s));
        }
    
        #[test]
        fn test_law2_jint_preview_then_review_gives_original() {
            let p = jint_prism();
            let s = Json::JInt(-7);
            assert!(check_preview_review(&p, &s));
        }
    
        #[test]
        fn test_law2_jnull_vacuously_true_for_jstring_prism() {
            let p = jstring_prism();
            assert!(check_preview_review(&p, &Json::JNull));
        }
    
        // --- Trait-based law checks ---
    
        #[test]
        fn test_trait_prism_law1_jstring() {
            assert!(JStringPrism::law_review_preview("rust".to_string()));
        }
    
        #[test]
        fn test_trait_prism_law2_jstring_matching_variant() {
            let s = Json::JString("optics".to_string());
            assert!(JStringPrism::law_preview_review(&s));
        }
    
        #[test]
        fn test_trait_prism_law2_jstring_non_matching_variant() {
            assert!(JStringPrism::law_preview_review(&Json::JBool(true)));
        }
    
        // --- Concrete behavior ---
    
        #[test]
        fn test_jstring_prism_preview_matching() {
            let p = jstring_prism();
            let j = Json::JString("abc".to_string());
            assert_eq!(p.preview(&j), Some("abc".to_string()));
        }
    
        #[test]
        fn test_jstring_prism_preview_non_matching() {
            let p = jstring_prism();
            assert_eq!(p.preview(&Json::JNull), None);
            assert_eq!(p.preview(&Json::JInt(1)), None);
        }
    
        #[test]
        fn test_jint_prism_review_then_preview() {
            let p = jint_prism();
            let constructed = p.review(100);
            assert_eq!(constructed, Json::JInt(100));
            assert_eq!(p.preview(&constructed), Some(100));
        }
    }

    Deep Comparison

    OCaml vs Rust: Prism Laws — ReviewPreview and PreviewReview

    Side-by-Side Code

    OCaml

    type ('s, 'a) prism = {
      preview : 's -> 'a option;
      review  : 'a -> 's;
    }
    
    (* Law 1 — ReviewPreview: preview (review a) = Some a *)
    let check_review_preview prism a =
      prism.preview (prism.review a) = Some a
    
    (* Law 2 — PreviewReview: if preview s = Some a then review a = s *)
    let check_preview_review prism s =
      match prism.preview s with
      | None   -> true   (* vacuously lawful *)
      | Some a -> prism.review a = s
    
    (* Lawful prism for JString *)
    let jstring_prism = {
      preview = (function JString s -> Some s | _ -> None);
      review  = (fun s -> JString s);
    }
    
    (* Unlawful prism — transforms value during preview *)
    let unlawful_uppercase = {
      preview = (function JString s -> Some (String.uppercase_ascii s) | _ -> None);
      review  = (fun s -> JString s);
    }
    

    Rust (idiomatic — struct with boxed closures)

    pub struct Prism<S, A> {
        preview: Box<dyn Fn(&S) -> Option<A>>,
        review:  Box<dyn Fn(A) -> S>,
    }
    
    /// Law 1 — ReviewPreview: preview(review(a)) == Some(a)
    pub fn check_review_preview<S, A>(prism: &Prism<S, A>, a: A) -> bool
    where
        A: PartialEq + Clone + 'static,
        S: 'static,
    {
        let s = prism.review(a.clone());
        prism.preview(&s) == Some(a)
    }
    
    /// Law 2 — PreviewReview: if preview(s) == Some(a) then review(a) == s
    pub fn check_preview_review<S, A>(prism: &Prism<S, A>, s: &S) -> bool
    where
        S: PartialEq + 'static,
        A: 'static,
    {
        match prism.preview(s) {
            None    => true,
            Some(a) => prism.review(a) == *s,
        }
    }
    
    pub fn jstring_prism() -> Prism<Json, String> {
        Prism::new(
            |j| match j { Json::JString(s) => Some(s.clone()), _ => None },
            Json::JString,
        )
    }
    
    pub fn unlawful_uppercase_prism() -> Prism<Json, String> {
        Prism::new(
            |j| match j { Json::JString(s) => Some(s.to_uppercase()), _ => None },
            Json::JString,
        )
    }
    

    Rust (functional — zero-cost trait dispatch)

    pub trait LawfulPrism {
        type Source: Clone + PartialEq;
        type Focus: Clone + PartialEq + 'static;
    
        fn preview(s: &Self::Source) -> Option<Self::Focus>;
        fn review(a: Self::Focus) -> Self::Source;
    
        fn law_review_preview(a: Self::Focus) -> bool {
            let s = Self::review(a.clone());
            Self::preview(&s) == Some(a)
        }
    
        fn law_preview_review(s: &Self::Source) -> bool {
            match Self::preview(s) {
                None    => true,
                Some(a) => Self::review(a) == *s,
            }
        }
    }
    
    pub struct JStringPrism;
    
    impl LawfulPrism for JStringPrism {
        type Source = Json;
        type Focus  = String;
        fn preview(j: &Json) -> Option<String> {
            match j { Json::JString(s) => Some(s.clone()), _ => None }
        }
        fn review(s: String) -> Json { Json::JString(s) }
    }
    

    Type Signatures

    ConceptOCamlRust
    Prism type('s, 'a) prism recordPrism<S, A> struct
    preview's -> 'a optionFn(&S) -> Option<A>
    review'a -> 'sFn(A) -> S
    Law 1 checker'a -> boolfn(prism, A) -> bool where A: PartialEq + Clone
    Law 2 checker's -> boolfn(prism, &S) -> bool where S: PartialEq
    Zero-cost variantN/A (modules-as-functors)trait LawfulPrism with associated types

    Key Insights

  • Laws are not enforced by the type system — only by tests. Both OCaml and Rust let you build a Prism from any two functions. The laws are semantic contracts you must verify explicitly with check_review_preview / check_preview_review; the compiler cannot check them for you.
  • The vacuous case matters. Law 2 ("PreviewReview") only fires when preview returns Some. Returning true on None is correct: the law says if preview succeeds, then round-trip holds. This asymmetry mirrors OCaml's pattern match directly.
  • Rust borrows, OCaml copies. OCaml's preview : 's -> 'a option takes ownership via value semantics. Rust's Fn(&S) -> Option<A> borrows S, which avoids cloning the outer type on every call — important when S is an enum with heap-allocated variants like Json.
  • **'static bounds on boxed closures are a Rust artifact.** The Box<dyn Fn(...)> approach requires 'static because the closure may outlive the scope where it was created. The trait-based approach (LawfulPrism) sidesteps this entirely: law checkers are inherent methods, so no lifetime constraint is needed on the prism itself.
  • Unlawful prisms expose why laws matter at scale. An unlawful prism (unlawful_uppercase_prism) passes most unit tests that only check one direction. Only the round-trip law check (check_review_preview) catches that review("hello") → JString("hello") and preview(JString("hello")) → Some("HELLO") disagree. In a composed pipeline of ten prisms, this silent corruption is nearly impossible to debug without law tests.
  • When to Use Each Style

    **Use closure-based Prism<S, A> when:** you need to build prisms at runtime (e.g., from configuration, user input, or a registry of optics). The boxing overhead is negligible compared to flexibility.

    **Use trait-based LawfulPrism when:** all prisms are known at compile time and you want zero-cost dispatch plus the law-check methods baked into the type. Each prism is a zero-sized marker struct; the compiler monomorphises every call.

    Exercises

  • Verify that the some_prism from example 206 is lawful by testing both laws with 100 random inputs.
  • Construct an unlawful prism that normalizes strings on review (lowercase) and show which law it violates.
  • Prove mathematically that law 2 implies that review is injective (different inputs produce different outputs).
  • Open Source Repos