Funx.Optics.Prism (funx v0.8.2)
View SourceThe Funx.Optics.Prism module provides a lawful partial optic for focusing on a branch of a data structure.
A prism is partial: the focus may or may not be present. This makes prisms ideal for working with
optional values, variants, and sum types. Unlike lenses, prisms never raise—they return Maybe instead.
When to use prisms vs lenses:
- Prisms (partial): Use for optional values, variants, sum types, missing map keys.
- Lenses (total): Use for record fields, map keys that always exist.
Constructors
key/1: Focuses on an optional key in a map.struct/1: Focuses on a specific struct type (for sum types).path/1: Focuses on nested paths through maps and structs.make/2: Creates a custom prism from preview and review functions.
Core Operations
preview/2: Attempts to extract the focus, returningJust(value)orNothing.review/2: Reconstructs the whole structure from the focused value.
Important: review constructs a fresh structure from the focused value alone—it does not merge
or preserve other fields. This is lawful prism behavior. If you need to update while preserving other
fields, use a lens instead.
Composition
compose/2: Composes two prisms sequentially (outer then inner).compose/1: Composes a list of prisms into a single prism.
Prisms compose naturally. Composing two prisms yields a new prism that attempts both matches in sequence,
stopping at the first Nothing.
Monoid Structure
Prisms form a monoid under composition for a fixed outer type s.
The monoid structure is provided via Funx.Monoid.Optics.PrismCompose, which wraps prisms
for use with generic monoid operations:
- Identity:
make(fn x -> Maybe.from_nil(x) end, fn x -> x end)- the identity prism - Operation:
compose/2- sequential composition
You can use compose/1 to compose multiple prisms sequentially, or work directly
with Funx.Monoid.Optics.PrismCompose for more control.
Examples
Working with optional map keys:
iex> name_prism = Funx.Optics.Prism.key(:name)
iex> Funx.Optics.Prism.preview(%{name: "Alice"}, name_prism)
%Funx.Monad.Maybe.Just{value: "Alice"}
iex> Funx.Optics.Prism.preview(%{age: 30}, name_prism)
%Funx.Monad.Maybe.Nothing{}Composing prisms for nested access:
iex> outer = Funx.Optics.Prism.key(:person)
iex> inner = Funx.Optics.Prism.key(:name)
iex> composed = Funx.Optics.Prism.compose(outer, inner)
iex> Funx.Optics.Prism.preview(%{person: %{name: "Alice"}}, composed)
%Funx.Monad.Maybe.Just{value: "Alice"}
iex> Funx.Optics.Prism.preview(%{person: %{age: 30}}, composed)
%Funx.Monad.Maybe.Nothing{}Using path/1 for convenient nested access:
iex> person_name = Funx.Optics.Prism.path([:person, :name])
iex> Funx.Optics.Prism.preview(%{person: %{name: "Alice"}}, person_name)
%Funx.Monad.Maybe.Just{value: "Alice"}
Summary
Functions
Composes prisms into a single prism using sequential composition.
Builds a prism that focuses on a single key inside a map.
Creates a custom prism from previewer and reviewer functions.
Builds a prism that focuses on a nested path through maps and structs.
Attempts to extract the focus from a structure using the prism.
Reconstructs the whole structure from the focused part.
Builds a prism that focuses on a specific struct constructor.
Types
Functions
Composes prisms into a single prism using sequential composition.
This delegates to the monoid append operation, which contains the canonical composition logic.
Binary composition
Composes two prisms. The outer prism runs first; if it succeeds, the inner prism runs next.
This is left-to-right composition: the first parameter is applied first. This differs from mathematical function composition (f ∘ g applies g first).
Sequential semantics:
- On
preview: Applies outer's matcher first, then inner's matcher (short-circuits onNothing) - On
review: Applies inner's builder first, then outer's builder
This is sequential matching through nested structures.
iex> outer = Funx.Optics.Prism.key(:account)
iex> inner = Funx.Optics.Prism.key(:name)
iex> p = Funx.Optics.Prism.compose(outer, inner)
iex> Funx.Optics.Prism.preview(%{account: %{name: "Alice"}}, p)
%Funx.Monad.Maybe.Just{value: "Alice"}List composition
Composes a list of prisms into a single prism using sequential composition.
Sequential semantics:
- On
preview: Applies matchers in list order (left-to-right), stopping at firstNothing - On
review: Applies builders in reverse list order (right-to-left)
This is not a union or choice operator. It does not "try all branches." It is strict sequential matching and construction.
iex> prisms = [
...> Funx.Optics.Prism.key(:account),
...> Funx.Optics.Prism.key(:name)
...> ]
iex> p = Funx.Optics.Prism.compose(prisms)
iex> Funx.Optics.Prism.preview(%{account: %{name: "Alice"}}, p)
%Funx.Monad.Maybe.Just{value: "Alice"}
iex> Funx.Optics.Prism.preview(%{other: %{name: "Bob"}}, p)
%Funx.Monad.Maybe.Nothing{}
Builds a prism that focuses on a single key inside a map.
Examples
iex> p = Funx.Optics.Prism.key(:name)
iex> Funx.Optics.Prism.preview(%{name: "Alice"}, p)
%Funx.Monad.Maybe.Just{value: "Alice"}
iex> Funx.Optics.Prism.preview(%{age: 30}, p)
%Funx.Monad.Maybe.Nothing{}
Creates a custom prism from previewer and reviewer functions.
The previewer attempts to extract the focused part, returning a Maybe.
The reviewer reconstructs the whole structure from the focused part.
Both functions must maintain the prism laws for the result to be lawful.
Examples
iex> p =
...> Funx.Optics.Prism.make(
...> fn x -> Funx.Monad.Maybe.just(x) end,
...> fn x -> x end
...> )
iex> Funx.Optics.Prism.preview(5, p)
%Funx.Monad.Maybe.Just{value: 5}
Builds a prism that focuses on a nested path through maps and structs.
Each element in the path can be:
:atom- A plain key access (works with maps and structs)Module- A naked struct verification (checks type, no key access){Module, :atom}- A struct-typed key access (verifies struct type and accesses key)
The syntax expands as follows:
:key→key(:key)- plain key accessModule→struct(Module)- struct type verification{Module, :key}→compose(struct(Module), key(:key))- typed field access
Modules are distinguished from plain keys using function_exported?(atom, :__struct__, 0).
Examples
# Plain map path
p1 = Prism.path([:person, :bio, :age])
Prism.review(30, p1)
#=> %{person: %{bio: %{age: 30}}}
# Given struct modules:
defmodule Bio do
defstruct [:age, :location]
end
defmodule Person do
defstruct [:name, :bio]
end
# Struct-typed path using {Module, :key} syntax
p2 = Prism.path([{Person, :bio}, {Bio, :age}])
Prism.review(30, p2)
#=> %Person{bio: %Bio{age: 30, location: nil}, name: nil}
# Naked struct at end verifies final type
p3 = Prism.path([:profile, Bio])
Prism.preview(%{profile: %Bio{age: 30}}, p3)
#=> Just(%Bio{age: 30, location: nil})
# Naked struct at beginning verifies root type
p4 = Prism.path([Person, :name])
Prism.review("Alice", p4)
#=> %Person{name: "Alice", bio: nil}
# Mix naked structs with typed field syntax
p5 = Prism.path([{Person, :bio}, Bio, :age])
Prism.review(25, p5)
#=> %Person{bio: %Bio{age: 25, location: nil}, name: nil}
# Naked struct only (just type verification)
p6 = Prism.path([Person])
Prism.preview(%Person{name: "Bob"}, p6)
#=> Just(%Person{name: "Bob", bio: nil})Implementation
The path/1 function composes prisms using compose/1:
:key→[key(:key)]Module→[struct(Module)]{Mod, :key}→[struct(Mod), key(:key)]
This means path is just syntactic sugar for prism composition.
Important
- When using
{Module, :field}, ensure:fieldexists inModule - Using non-existent fields may violate prism laws (Kernel.struct/2 silently drops invalid keys)
- The tuple form
{Module, :key}requiresModuleto be a struct module (raises otherwise) - Plain lowercase atoms like
:userare always treated as keys, not struct modules
@spec preview(s, t(s, a)) :: Funx.Monad.Maybe.t(a) when s: term(), a: term()
Attempts to extract the focus from a structure using the prism.
Returns a Funx.Monad.Maybe.Just on success or Funx.Monad.Maybe.Nothing
if the branch does not match.
Examples
iex> p = Funx.Optics.Prism.key(:name)
iex> Funx.Optics.Prism.preview(%{name: "Alice"}, p)
%Funx.Monad.Maybe.Just{value: "Alice"}
iex> Funx.Optics.Prism.preview(%{age: 30}, p)
%Funx.Monad.Maybe.Nothing{}
Reconstructs the whole structure from the focused part.
Review reverses the prism, injecting the focused value back into the outer
structure. Important: review constructs a fresh structure from the
focused value alone - it does not merge with or patch an existing structure.
This is the lawful behaviour of prisms.
If you need to update a field while preserving other fields, you need a lens, not a prism.
Note: Cannot review with nil as it would violate prism laws (since
Just(nil) is invalid).
Examples
iex> p = Funx.Optics.Prism.key(:name)
iex> Funx.Optics.Prism.review("Alice", p)
%{name: "Alice"}
Builds a prism that focuses on a specific struct constructor.
This prism succeeds only when the input value is a struct of the given module. It models a sum-type constructor: selecting one structural variant from a set of possible variants.
On review, this prism can promote a plain map to the specified struct type,
filling in defaults for missing fields.
Examples
# Given a struct module:
defmodule Account do
defstruct [:name, :email]
end
# Create a prism for that struct type
p = Prism.struct(Account)
# Preview succeeds for matching struct
Prism.preview(%Account{name: "Alice"}, p)
#=> %Just{value: %Account{name: "Alice", email: nil}}
# Preview fails for non-matching types
Prism.preview(%{name: "Bob"}, p)
#=> %Nothing{}
# Review promotes a map to the struct type
Prism.review(%{name: "Charlie"}, p)
#=> %Account{name: "Charlie", email: nil}Composition
The struct/1 prism is commonly composed with key/1 to focus on struct fields:
user_name = Prism.compose(Prism.struct(Account), Prism.key(:name))
Prism.review("Alice", user_name)
#=> %Account{name: "Alice", email: nil}