Funx.Optics.Traversal (funx v0.8.2)

View Source

Run in Livebook

The Funx.Optics.Traversal module provides a multi-focus optic for targeting multiple locations in a data structure.

A traversal is built using combine, which takes multiple optics (Lens or Prism) and creates a single optic that can focus on all of them as a single optic.

Building Traversals

  • combine/1: Takes a list of optics and creates a multi-focus traversal.

Read Operations

  • to_list/2: Extracts values from lens foci and any prism foci that match.
  • to_list_maybe/2: Extracts values from all foci (all-or-nothing).
  • preview/2: Returns the first matching focus.
  • has/2: Returns true if at least one focus matches.

Key Properties

  • Order preservation: Foci are traversed in the order they were combined.
  • Lens behavior: Lens foci require presence and raise on violation.
  • Prism behavior: Prism foci contribute if they match, otherwise are skipped.
  • combine is a monoid: Declares multiplicity, not iteration.

Examples

iex> alias Funx.Optics.{Lens, Prism, Traversal}
iex> t = Traversal.combine([Lens.key(:name), Lens.key(:age)])
iex> Traversal.to_list(%{name: "Alice", age: 30}, t)
["Alice", 30]

With Prisms (optional foci):

iex> alias Funx.Optics.{Lens, Prism, Traversal}
iex> t = Traversal.combine([Lens.key(:name), Prism.key(:email)])
iex> Traversal.to_list(%{name: "Alice"}, t)
["Alice"]
iex> Traversal.to_list(%{name: "Alice", email: "alice@example.com"}, t)
["Alice", "alice@example.com"]

Summary

Functions

Combines multiple optics into a single multi-focus traversal.

Returns true if at least one focus matches.

Returns the first successful focus from a traversal.

Extracts values from all foci into a list.

Extracts values from all foci into a Maybe list (all-or-nothing).

Types

t()

@type t() :: %Funx.Optics.Traversal{
  foci: [Funx.Optics.Lens.t() | Funx.Optics.Prism.t()]
}

Functions

combine(optics)

@spec combine([Funx.Optics.Lens.t() | Funx.Optics.Prism.t()]) :: t()

Combines multiple optics into a single multi-focus traversal.

This is parallel composition. It widens the focus to include all provided optics. The resulting traversal targets all foci simultaneously.

Examples

iex> alias Funx.Optics.{Lens, Traversal}
iex> t = Traversal.combine([Lens.key(:name), Lens.key(:age)])
iex> Traversal.to_list(%{name: "Alice", age: 30}, t)
["Alice", 30]

With composed paths:

iex> alias Funx.Optics.{Lens, Traversal}
iex> path = Lens.compose([Lens.key(:user), Lens.key(:name)])
iex> t = Traversal.combine([path, Lens.key(:score)])
iex> Traversal.to_list(%{user: %{name: "Bob"}, score: 100}, t)
["Bob", 100]

Empty traversal (identity):

iex> alias Funx.Optics.Traversal
iex> t = Traversal.combine([])
iex> Traversal.to_list(%{name: "Alice"}, t)
[]

has(structure, traversal)

@spec has(s, t()) :: boolean() when s: term()

Returns true if at least one focus matches.

This is a boolean query derived from preview/2:

  • Returns true if any focus matches
  • Returns false if all foci fail (Nothing)
  • Lens throws on contract violation

Examples

iex> alias Funx.Optics.{Prism, Traversal}
iex> t = Traversal.combine([Prism.key(:name)])
iex> Traversal.has(%{name: "Alice"}, t)
true

No match:

iex> alias Funx.Optics.{Prism, Traversal}
iex> t = Traversal.combine([Prism.key(:email)])
iex> Traversal.has(%{name: "Alice"}, t)
false

Empty traversal:

iex> alias Funx.Optics.Traversal
iex> t = Traversal.combine([])
iex> Traversal.has(%{name: "Alice"}, t)
false

preview(structure, traversal)

@spec preview(s, t()) :: Funx.Monad.Maybe.t(a) when s: term(), a: term()

Returns the first successful focus from a traversal.

Collapses multiple foci to at most one value using first-success semantics:

  • Returns the first Just and ignores later matches
  • Prism Nothing is skipped
  • Lens throws on contract violation
  • Traversal order determines priority

Examples

iex> alias Funx.Optics.{Lens, Prism, Traversal}
iex> alias Funx.Monad.Maybe
iex> t = Traversal.combine([Prism.key(:email), Prism.key(:name)])
iex> Traversal.preview(%{name: "Alice"}, t)
%Maybe.Just{value: "Alice"}

First success wins:

iex> alias Funx.Optics.{Prism, Traversal}
iex> alias Funx.Monad.Maybe
iex> t = Traversal.combine([Prism.key(:name), Prism.key(:email)])
iex> Traversal.preview(%{name: "Alice", email: "alice@example.com"}, t)
%Maybe.Just{value: "Alice"}

Nothing when no foci match:

iex> alias Funx.Optics.{Prism, Traversal}
iex> alias Funx.Monad.Maybe
iex> t = Traversal.combine([Prism.key(:email), Prism.key(:phone)])
iex> Traversal.preview(%{name: "Alice"}, t)
%Maybe.Nothing{}

Lens throws on violation:

iex> alias Funx.Optics.{Lens, Traversal}
iex> t = Traversal.combine([Lens.key(:email)])
iex> Traversal.preview(%{name: "Alice"}, t)
** (KeyError) key :email not found in: %{name: "Alice"}

to_list(structure, traversal)

@spec to_list(s, t()) :: [a] when s: term(), a: term()

Extracts values from all foci into a list.

For each focus in the traversal:

  • Lens: Uses view!, contributes one value or throws on contract violation
  • Prism: Uses preview, contributes one value if matches, otherwise skips (Nothing)

The order of values matches the combine order.

Examples

iex> alias Funx.Optics.{Lens, Traversal}
iex> t = Traversal.combine([Lens.key(:name), Lens.key(:age)])
iex> Traversal.to_list(%{name: "Alice", age: 30}, t)
["Alice", 30]

With Prisms (skips Nothing):

iex> alias Funx.Optics.{Prism, Traversal}
iex> t = Traversal.combine([Prism.key(:name), Prism.key(:email)])
iex> Traversal.to_list(%{name: "Alice"}, t)
["Alice"]

Order is preserved:

iex> alias Funx.Optics.{Lens, Traversal}
iex> t = Traversal.combine([Lens.key(:age), Lens.key(:name)])
iex> Traversal.to_list(%{name: "Alice", age: 30}, t)
[30, "Alice"]

Lens contract violation throws:

iex> alias Funx.Optics.{Lens, Traversal}
iex> t = Traversal.combine([Lens.key(:name)])
iex> Traversal.to_list(%{age: 30}, t)
** (KeyError) key :name not found in: %{age: 30}

to_list_maybe(structure, traversal)

@spec to_list_maybe(s, t()) :: Funx.Monad.Maybe.t([a]) when s: term(), a: term()

Extracts values from all foci into a Maybe list (all-or-nothing).

This is the all-or-nothing version of to_list/2. Unlike to_list/2 which skips prism foci that don't match, this operation returns Nothing if any prism focus doesn't match.

For each focus in the traversal:

  • Lens: Uses view!, contributes one value or throws on contract violation
  • Prism: Uses preview, contributes one value if matches, otherwise returns Nothing for the entire operation

Returns Just(list) only when every focus succeeds.

This is useful for enforcing co-presence: "this structure exists in ALL these contexts."

Examples

iex> alias Funx.Optics.{Lens, Prism, Traversal}
iex> alias Funx.Monad.Maybe
iex> t = Traversal.combine([Prism.key(:name), Prism.key(:email)])
iex> Traversal.to_list_maybe(%{name: "Alice", email: "alice@example.com"}, t)
%Maybe.Just{value: ["Alice", "alice@example.com"]}

Returns Nothing if any Prism doesn't match:

iex> alias Funx.Optics.{Prism, Traversal}
iex> alias Funx.Monad.Maybe
iex> t = Traversal.combine([Prism.key(:name), Prism.key(:email)])
iex> Traversal.to_list_maybe(%{name: "Alice"}, t)
%Maybe.Nothing{}

Lens contract violation throws:

iex> alias Funx.Optics.{Lens, Traversal}
iex> t = Traversal.combine([Lens.key(:name)])
iex> Traversal.to_list_maybe(%{age: 30}, t)
** (KeyError) key :name not found in: %{age: 30}