Funx.Optics.Iso (funx v0.8.0)
View SourceThe Funx.Optics.Iso module provides a lawful isomorphism optic for bidirectional, lossless transformations.
An isomorphism (iso) represents a reversible transformation between two types. It consists of two inverse functions that satisfy the round-trip laws:
review(view(s, iso), iso) == s- Round-trip forward then back returns the originalview(review(a, iso), iso) == a- Round-trip back then forward returns the original
Isos are total optics with no partiality. If the transformation can fail, you do not have an iso. Contract violations crash immediately - there are no bang variants or safe alternatives.
Constructors
make/2: Creates a custom iso from two inverse functions.identity/0: The identity iso (both directions are identity).
Core Operations
view/2: Apply the forward transformation (s -> a).review/2: Apply the backward transformation (a -> s).over/3: Modify the viewed side (view, apply function, review).under/3: Modify the reviewed side (review, apply function, view).
Direction
from/1: Reverse the iso's direction.
Composition
compose/2: Composes two isos sequentially (outer then inner).compose/1: Composes a list of isos into a single iso.
Interoperability
as_lens/1: Converts an iso to a lens.as_prism/1: Converts an iso to a prism.
An iso is more powerful than both lens and prism. Every iso can be used as a lens
(viewing and setting always succeed) or as a prism (preview always returns Just).
Isos compose naturally. Composing two isos yields a new iso where:
- Forward (
view) applies the outer iso first, then the inner iso - Backward (
review) applies the inner iso first, then the outer iso
Monoid Structure
Isos form a monoid under composition.
The monoid structure is provided via Funx.Monoid.Optics.IsoCompose, which wraps isos
for use with generic monoid operations:
- Identity:
identity/0- the identity iso - Operation:
compose/2- sequential composition
Composing an empty list returns the identity iso.
Examples
Simple encoding/decoding:
iex> alias Funx.Optics.Iso
iex> # Iso between string and integer (string representation)
iex> string_int = Iso.make(
...> fn s -> String.to_integer(s) end,
...> fn i -> Integer.to_string(i) end
...> )
iex> Iso.view("42", string_int)
42
iex> Iso.review(42, string_int)
"42"Composing isos:
iex> alias Funx.Optics.Iso
iex> # Iso: string <-> integer
iex> string_int = Iso.make(
...> fn s -> String.to_integer(s) end,
...> fn i -> Integer.to_string(i) end
...> )
iex> # Iso: integer <-> doubled integer
iex> double = Iso.make(
...> fn i -> i * 2 end,
...> fn i -> div(i, 2) end
...> )
iex> # Composed: string <-> doubled integer
iex> composed = Iso.compose(string_int, double)
iex> Iso.view("21", composed)
42
iex> Iso.review(42, composed)
"21"Using over and under:
iex> alias Funx.Optics.Iso
iex> string_int = Iso.make(
...> fn s -> String.to_integer(s) end,
...> fn i -> Integer.to_string(i) end
...> )
iex> Iso.over("10", string_int, fn i -> i * 5 end)
"50"
iex> Iso.under(100, string_int, fn s -> s <> "0" end)
1000
Summary
Functions
Converts an iso to a lens.
Converts an iso to a prism.
Composes isos into a single iso using sequential composition.
Reverses the direction of an iso.
The identity iso that leaves values unchanged in both directions.
Creates a custom iso from two inverse functions.
Modify the viewed side of the iso.
Apply the backward transformation of the iso.
Modify the reviewed side of the iso.
Apply the forward transformation of the iso.
Types
Functions
@spec as_lens(t(s, a)) :: Funx.Optics.Lens.t(s, a) when s: term(), a: term()
Converts an iso to a lens.
An iso is more powerful than a lens: it provides bidirectional transformation, while a lens only provides viewing and updating. Every iso can be used as a lens.
The resulting lens:
viewuses the iso's forward transformationupdateignores the old value and uses the iso's backward transformation
This is safe because an iso is total - the transformation always succeeds.
Examples
iex> alias Funx.Optics.{Iso, Lens}
iex> string_int = Iso.make(
...> fn s -> String.to_integer(s) end,
...> fn i -> Integer.to_string(i) end
...> )
iex> lens = Iso.as_lens(string_int)
iex> Lens.view!("42", lens)
42
iex> Lens.set!("10", lens, 99)
"99"
@spec as_prism(t(s, a)) :: Funx.Optics.Prism.t(s, a) when s: term(), a: term()
Converts an iso to a prism.
An iso is more powerful than a prism: it never fails to extract a value, while a prism models optional extraction. Every iso can be used as a prism.
The resulting prism:
previewalways succeeds (returnsJust), using the iso's forward transformationreviewuses the iso's backward transformation
This is safe because an iso is total - the transformation always succeeds.
Examples
iex> alias Funx.Optics.{Iso, Prism}
iex> alias Funx.Monad.Maybe.Just
iex> string_int = Iso.make(
...> fn s -> String.to_integer(s) end,
...> fn i -> Integer.to_string(i) end
...> )
iex> prism = Iso.as_prism(string_int)
iex> Prism.preview("42", prism)
%Just{value: 42}
iex> Prism.review(42, prism)
"42"
Composes isos into a single iso using sequential composition.
This delegates to the monoid append operation, which contains the canonical composition logic.
Binary composition
Composes two isos. The outer iso transforms first, then the inner iso transforms 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 forward transformation first, then inner's forward transformation - On
review: Applies inner's backward transformation first, then outer's backward transformation
This is sequential transformation through composed isos.
iex> alias Funx.Optics.Iso
iex> # string <-> int
iex> string_int = Iso.make(
...> fn s -> String.to_integer(s) end,
...> fn i -> Integer.to_string(i) end
...> )
iex> # int <-> doubled int
iex> double = Iso.make(
...> fn i -> i * 2 end,
...> fn i -> div(i, 2) end
...> )
iex> composed = Iso.compose(string_int, double)
iex> Iso.view("21", composed)
42
iex> Iso.review(42, composed)
"21"List composition
Composes a list of isos into a single iso using sequential composition.
Sequential semantics:
- On
view: Applies transformations in list order (left-to-right) - On
review: Applies transformations in reverse list order (right-to-left)
This is sequential transformation through composed isos.
iex> isos = [
...> Funx.Optics.Iso.make(
...> fn s -> String.to_integer(s) end,
...> fn i -> Integer.to_string(i) end
...> ),
...> Funx.Optics.Iso.make(
...> fn i -> i * 2 end,
...> fn i -> div(i, 2) end
...> )
...> ]
iex> composed = Funx.Optics.Iso.compose(isos)
iex> Funx.Optics.Iso.view("21", composed)
42
Reverses the direction of an iso.
Swaps the view and review functions.
This is the established optic operation for reversing direction, following Haskell's
Control.Lens.Iso.from.
Examples
iex> string_int = Funx.Optics.Iso.make(
...> fn s -> String.to_integer(s) end,
...> fn i -> Integer.to_string(i) end
...> )
iex> int_string = Funx.Optics.Iso.from(string_int)
iex> Funx.Optics.Iso.view(42, int_string)
"42"
iex> Funx.Optics.Iso.review("42", int_string)
42
@spec identity() :: t()
The identity iso that leaves values unchanged in both directions.
Examples
iex> iso = Funx.Optics.Iso.identity()
iex> Funx.Optics.Iso.view(42, iso)
42
iex> Funx.Optics.Iso.review(42, iso)
42
Creates a custom iso from two inverse functions.
The viewer function transforms from the source type to the target type. The reviewer function transforms from the target type back to the source type.
Both functions must be inverses for the iso to be lawful:
review(view(s, iso), iso) == sview(review(a, iso), iso) == a
If these functions are not true inverses, the iso contract is violated and the program is incorrect. There are no runtime checks - the contract is enforced by design.
Examples
iex> # Celsius <-> Fahrenheit
iex> temp_iso = Funx.Optics.Iso.make(
...> fn c -> c * 9 / 5 + 32 end,
...> fn f -> (f - 32) * 5 / 9 end
...> )
iex> Funx.Optics.Iso.view(0, temp_iso)
32.0
iex> Funx.Optics.Iso.review(32, temp_iso)
0.0
Modify the viewed side of the iso.
Applies a function through the iso: view, apply function, review.
This is the standard optic modifier, consistent with Lens and Prism.
Examples
iex> string_int = Funx.Optics.Iso.make(
...> fn s -> String.to_integer(s) end,
...> fn i -> Integer.to_string(i) end
...> )
iex> Funx.Optics.Iso.over("10", string_int, fn i -> i * 5 end)
"50"
Apply the backward transformation of the iso.
Transforms from the target type back to the source type.
This operation is total. If it crashes, the iso contract is violated.
Examples
iex> string_int = Funx.Optics.Iso.make(
...> fn s -> String.to_integer(s) end,
...> fn i -> Integer.to_string(i) end
...> )
iex> Funx.Optics.Iso.review(42, string_int)
"42"
Modify the reviewed side of the iso.
Applies a function in reverse through the iso: review, apply function, view.
This operation is unique to Iso due to its bidirectional symmetry. Lens and Prism cannot offer this.
Examples
iex> string_int = Funx.Optics.Iso.make(
...> fn s -> String.to_integer(s) end,
...> fn i -> Integer.to_string(i) end
...> )
iex> Funx.Optics.Iso.under(100, string_int, fn s -> s <> "0" end)
1000
Apply the forward transformation of the iso.
Transforms from the source type to the target type.
This operation is total. If it crashes, the iso contract is violated.
Examples
iex> string_int = Funx.Optics.Iso.make(
...> fn s -> String.to_integer(s) end,
...> fn i -> Integer.to_string(i) end
...> )
iex> Funx.Optics.Iso.view("42", string_int)
42