Funx.Optics.Lens (funx v0.8.2)
View SourceThe Funx.Optics.Lens module provides a lawful total optic for focusing on a part of a data structure.
A lens is total: it assumes the focus always exists within the valid domain. This is a contract
enforced at runtime by raising KeyError when violated. If the focus might not exist, use a prism instead.
Both view! and set! enforce totality symmetrically for all data types (maps, structs, etc.). If either
operation can succeed when the focus is missing, you no longer have a lens.
Constructors
key/1: Focuses on a single key in a map or struct.path/1: Focuses on nested keys by composingkey/1lenses.make/2: Creates a custom lens from viewer and updater functions.
Core Operations
view!/2: Extracts the focused part (raisesKeyErrorif missing).set!/3: Updates the focused part (raisesKeyErrorif missing).over!/3: Applies a function to the focused part (raisesKeyErrorif missing).
Safe Operations
view/3: Safe version ofview!/2(returnsEitheror tuple).set/4: Safe version ofset!/3(returnsEitheror tuple).over/4: Safe version ofover!/3(returnsEitheror tuple).
Error handling modes:
Safe operations accept an optional :as parameter:
:either(default): ReturnsRight(value)orLeft(exception).:tuple: Returns{:ok, value}or{:error, exception}.:raise: Behaves like the!version, raising exceptions directly.
Safe operations use Either.from_try/1 internally, which catches all exceptions,
not just KeyError.
Composition
compose/2: Composes two lenses sequentially (outer then inner).compose/1: Composes a list of lenses into a single lens.
Lenses compose naturally. Composing two lenses yields a new lens that focuses through both layers sequentially.
Monoid Structure
Lenses form a monoid under composition for a fixed outer type s.
The monoid structure is provided via Funx.Monoid.Optics.LensCompose, which wraps lenses
for use with generic monoid operations:
- Identity:
make(fn s -> s end, fn _s, a -> a end)- the identity lens - Operation:
compose/2- sequential composition
You can use compose/1 to compose multiple lenses sequentially, or work directly
with Funx.Monoid.Optics.LensCompose for more control.
Examples
iex> alias Funx.Optics.Lens
iex> lens = Lens.key(:age)
iex> %{age: 40} |> Lens.view!(lens)
40
iex> %{age: 40} |> Lens.set!(lens, 50)
%{age: 50}Composing lenses for nested access:
iex> alias Funx.Optics.Lens
iex> outer = Lens.key(:profile)
iex> inner = Lens.key(:score)
iex> lens = Lens.compose(outer, inner)
iex> %{profile: %{score: 12}} |> Lens.view!(lens)
12
iex> %{profile: %{score: 12}} |> Lens.set!(lens, 99)
%{profile: %{score: 99}}Deeply nested composition with compose/1:
iex> alias Funx.Optics.Lens
iex> lens = Lens.compose([Lens.key(:stats), Lens.key(:wins)])
iex> %{stats: %{wins: 7}} |> Lens.view!(lens)
7
iex> %{stats: %{wins: 7}} |> Lens.set!(lens, 8)
%{stats: %{wins: 8}}
Summary
Functions
Composes lenses into a single lens using sequential composition.
Builds a lawful lens focusing on a single key in a map or struct.
Creates a custom lens from viewer and updater functions.
Safe version of over!/3 that returns an Either or tuple instead of raising.
Updates the focused part of a structure by applying a function to it.
Builds a lawful lens for nested map access by composing key/1 lenses.
Safe version of set!/3 that returns an Either or tuple instead of raising.
Updates the focused part of a structure by setting it to a new value.
Safe version of view!/2 that returns an Either or tuple instead of raising.
Extracts the focused part of a structure using a lens.
Types
Functions
Composes lenses into a single lens using sequential composition.
This delegates to the monoid append operation, which contains the canonical composition logic.
Binary composition
Composes two lenses. The outer lens focuses first, then the inner lens focuses within the result.
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
view!: Applies outer's viewer first, then inner's viewer - On
set!: Updates through both lenses, maintaining nested structure
This is sequential focusing through nested structures.
iex> alias Funx.Optics.Lens
iex> outer = Lens.key(:profile)
iex> inner = Lens.key(:age)
iex> lens = Lens.compose(outer, inner)
iex> %{profile: %{age: 30}} |> Lens.view!(lens)
30List composition
Composes a list of lenses into a single lens using sequential composition.
Sequential semantics:
- On
view!: Applies viewers in list order (left-to-right) - On
set!: Updates through each lens in reverse order to maintain structure
This is sequential focusing through nested structures.
iex> lenses = [
...> Funx.Optics.Lens.key(:user),
...> Funx.Optics.Lens.key(:profile),
...> Funx.Optics.Lens.key(:age)
...> ]
iex> lens = Funx.Optics.Lens.compose(lenses)
iex> %{user: %{profile: %{age: 25}}} |> Funx.Optics.Lens.view!(lens)
25
Builds a lawful lens focusing on a single key in a map or struct.
Contract
The key must exist in the structure. When used with view! and set!,
this lens uses Map.fetch!/2 and Map.replace!/3, raising KeyError if
the key is missing. This symmetric enforcement ensures all three lens laws hold.
If the key might not exist, use a prism instead.
Type Note
The return type t(map(), term()) uses Elixir's map() type, which includes
both plain maps and structs (since structs are maps with a __struct__ key).
Examples
iex> lens = Funx.Optics.Lens.key(:name)
iex> %{name: "Alice"} |> Funx.Optics.Lens.view!(lens)
"Alice"
iex> %{name: "Alice"} |> Funx.Optics.Lens.set!(lens, "Bob")
%{name: "Bob"}Works with string keys:
iex> lens = Funx.Optics.Lens.key("count")
iex> %{"count" => 5} |> Funx.Optics.Lens.view!(lens)
5With structs (preserves type):
defmodule User, do: defstruct [:name, :age]
lens = Funx.Optics.Lens.key(:name)
user = %User{name: "Alice", age: 30}
Funx.Optics.Lens.view!(user, lens) #=> "Alice"
Funx.Optics.Lens.set!(user, lens, "Bob") #=> %User{name: "Bob", age: 30}
Creates a custom lens from viewer and updater functions.
The viewer extracts the focused part from the structure. The updater takes the structure and a new value, returning an updated structure.
Both functions must maintain the lens laws for the result to be lawful.
Examples
iex> # A lens that views and updates the length of a string
iex> lens = Funx.Optics.Lens.make(
...> fn s -> String.length(s) end,
...> fn s, len -> String.duplicate(s, div(len, String.length(s))) end
...> )
iex> Funx.Optics.Lens.view!("hello", lens)
5
@spec over(s, t(s, a), (a -> a), keyword()) :: Funx.Monad.Either.t(any(), s) | {:ok, s} | {:error, any()} | s when s: term(), a: term()
Safe version of over!/3 that returns an Either or tuple instead of raising.
See the "Safe Operations" section in the module documentation for details about error handling modes and what exceptions are caught.
Examples
iex> lens = Funx.Optics.Lens.key(:age)
iex> Funx.Optics.Lens.over(%{age: 30}, lens, fn a -> a + 1 end)
%Funx.Monad.Either.Right{right: %{age: 31}}
iex> lens = Funx.Optics.Lens.key(:age)
iex> Funx.Optics.Lens.over(%{}, lens, fn a -> a + 1 end)
%Funx.Monad.Either.Left{left: %KeyError{key: :age, term: %{}}}
iex> lens = Funx.Optics.Lens.key(:score)
iex> Funx.Optics.Lens.over(%{score: 10}, lens, fn s -> s * 2 end, as: :tuple)
{:ok, %{score: 20}}
iex> lens = Funx.Optics.Lens.key(:value)
iex> Funx.Optics.Lens.over(%{value: 5}, lens, fn v -> v + 1 end, as: :raise)
%{value: 6}
Updates the focused part of a structure by applying a function to it.
This is the derived transformation operation for a lens. It is implemented as:
Because lenses are total, over!/3 is also total. If the focus does not
exist, a KeyError is raised by view!/2 or set!/3.
Only the focused part is changed. All other structure and data is preserved.
Examples
iex> lens = Funx.Optics.Lens.key(:age)
iex> data = %{age: 40}
iex> Funx.Optics.Lens.over!(data, lens, fn a -> a + 1 end)
%{age: 41}Works through composed lenses:
iex> outer = Funx.Optics.Lens.key(:profile)
iex> inner = Funx.Optics.Lens.key(:score)
iex> lens = Funx.Optics.Lens.compose(outer, inner)
iex> data = %{profile: %{score: 10}}
iex> Funx.Optics.Lens.over!(data, lens, fn s -> s * 2 end)
%{profile: %{score: 20}}Works through path/1:
iex> lens = Funx.Optics.Lens.path([:stats, :wins])
iex> data = %{stats: %{wins: 3}}
iex> Funx.Optics.Lens.over!(data, lens, fn n -> n + 5 end)
%{stats: %{wins: 8}}
Builds a lawful lens for nested map access by composing key/1 lenses.
This is equivalent to compose(Enum.map(keys, &key/1)) and enforces totality
at every level - raising KeyError when used with view! or set! if any
intermediate key is missing.
Type Note
The return type t(map(), term()) uses Elixir's map() type, which includes
both plain maps and structs (since structs are maps with a __struct__ key).
Examples
iex> lens = Funx.Optics.Lens.path([:user, :profile, :name])
iex> data = %{user: %{profile: %{name: "Alice"}}}
iex> Funx.Optics.Lens.view!(data, lens)
"Alice"
iex> Funx.Optics.Lens.set!(data, lens, "Bob")
%{user: %{profile: %{name: "Bob"}}}Raises on missing keys when accessed:
iex> lens = Funx.Optics.Lens.path([:user, :name])
iex> Funx.Optics.Lens.view!(%{}, lens)
** (KeyError) key :user not found in: %{}
@spec set(s, t(s, a), a, keyword()) :: Funx.Monad.Either.t(any(), s) | {:ok, s} | {:error, any()} | s when s: term(), a: term()
Safe version of set!/3 that returns an Either or tuple instead of raising.
See the "Safe Operations" section in the module documentation for details about error handling modes and what exceptions are caught.
Examples
iex> lens = Funx.Optics.Lens.key(:age)
iex> Funx.Optics.Lens.set(%{age: 30}, lens, 31)
%Funx.Monad.Either.Right{right: %{age: 31}}
iex> lens = Funx.Optics.Lens.key(:age)
iex> Funx.Optics.Lens.set(%{}, lens, 31)
%Funx.Monad.Either.Left{left: %KeyError{key: :age, term: %{}}}
iex> lens = Funx.Optics.Lens.key(:count)
iex> Funx.Optics.Lens.set(%{count: 5}, lens, 10, as: :tuple)
{:ok, %{count: 10}}
iex> lens = Funx.Optics.Lens.key(:name)
iex> Funx.Optics.Lens.set(%{name: "Alice"}, lens, "Bob", as: :raise)
%{name: "Bob"}
Updates the focused part of a structure by setting it to a new value.
Raises KeyError if the focus does not exist. The entire structure is
returned with only the focused part changed. All other fields and nested
structures are preserved. Struct types are maintained.
For non-raising behavior, use set/4 instead.
Examples
iex> lens = Funx.Optics.Lens.key(:age)
iex> Funx.Optics.Lens.set!(%{age: 30, name: "Alice"}, lens, 31)
%{age: 31, name: "Alice"}
iex> lens = Funx.Optics.Lens.key(:age)
iex> Funx.Optics.Lens.set!(%{name: "Alice"}, lens, 31)
** (KeyError) key :age not found in: %{name: "Alice"}
@spec view(s, t(s, a), keyword()) :: Funx.Monad.Either.t(any(), a) | {:ok, a} | {:error, any()} | a when s: term(), a: term()
Safe version of view!/2 that returns an Either or tuple instead of raising.
See the "Safe Operations" section in the module documentation for details about error handling modes and what exceptions are caught.
Examples
iex> lens = Funx.Optics.Lens.key(:name)
iex> Funx.Optics.Lens.view(%{name: "Alice"}, lens)
%Funx.Monad.Either.Right{right: "Alice"}
iex> lens = Funx.Optics.Lens.key(:name)
iex> Funx.Optics.Lens.view(%{}, lens)
%Funx.Monad.Either.Left{left: %KeyError{key: :name, term: %{}}}
iex> lens = Funx.Optics.Lens.key(:age)
iex> Funx.Optics.Lens.view(%{age: 30}, lens, as: :tuple)
{:ok, 30}
iex> lens = Funx.Optics.Lens.key(:age)
iex> Funx.Optics.Lens.view(%{age: 30}, lens, as: :raise)
30
Extracts the focused part of a structure using a lens.
Raises KeyError if the focus does not exist (e.g., missing map key).
For non-raising behavior, use view/3 instead.
Examples
iex> lens = Funx.Optics.Lens.key(:name)
iex> Funx.Optics.Lens.view!(%{name: "Alice"}, lens)
"Alice"
iex> lens = Funx.Optics.Lens.key(:name)
iex> Funx.Optics.Lens.view!(%{}, lens)
** (KeyError) key :name not found in: %{}