Current section
Files
Jump to
Current section
Files
lib/monoid/optics/lens_compose.ex
defmodule Funx.Monoid.Optics.LensCompose do
@moduledoc """
The `Funx.Monoid.Optics.LensCompose` module provides a monoid wrapper for sequential lens composition.
This wrapper allows lenses to be used with generic monoid operations like `m_concat/2` and `m_append/3`,
enabling functional composition of multiple lenses into a single focusing operation.
### Wrapping and Unwrapping
- `new/1`: Wraps a lens in a `LensCompose` monoid.
- `unwrap/1`: Extracts the lens from a `LensCompose` wrapper.
### Monoid Operations (via protocol)
- `empty/1`: Returns the identity lens (leaves structure unchanged).
- `append/2`: Composes two lenses sequentially (outer then inner).
- `wrap/2`: Wraps a lens value into the monoid.
## Examples
iex> alias Funx.Monoid.Optics.LensCompose
iex> alias Funx.Optics.Lens
iex> lenses = [
...> Lens.key(:profile),
...> Lens.key(:score)
...> ]
iex> wrapped = Enum.map(lenses, &LensCompose.new/1)
iex> composed = Funx.Monoid.Utils.m_concat(%LensCompose{}, lenses)
iex> %{profile: %{score: 42}} |> Lens.view!(composed)
42
"""
alias Funx.Optics.Lens
@type t :: %__MODULE__{
lens: Lens.t()
}
defstruct lens: nil
@doc """
Wraps a lens in a LensCompose monoid.
"""
@spec new(Lens.t()) :: t()
def new(%Lens{} = lens), do: %__MODULE__{lens: lens}
@doc """
Extracts the lens from a LensCompose wrapper.
"""
@spec unwrap(t()) :: Lens.t()
def unwrap(%__MODULE__{lens: lens}), do: lens
end
defimpl Funx.Monoid, for: Funx.Monoid.Optics.LensCompose do
alias Funx.Monoid.Optics.LensCompose
alias Funx.Optics.Lens
@doc """
Returns the identity lens (views and updates the whole structure unchanged).
"""
def empty(_) do
LensCompose.new(Funx.Optics.Lens.identity())
end
@doc """
Composes two lenses sequentially.
The outer lens runs first, then the inner lens focuses within.
This is the canonical implementation of lens composition.
"""
def append(%LensCompose{lens: outer}, %LensCompose{lens: inner}) do
composed =
Lens.make(
fn s ->
s |> Lens.view!(outer) |> Lens.view!(inner)
end,
fn s, a ->
inner_struct = Lens.view!(s, outer)
updated_inner = Lens.set!(inner_struct, inner, a)
Lens.set!(s, outer, updated_inner)
end
)
LensCompose.new(composed)
end
@doc """
Wraps a lens value into a LensCompose monoid.
"""
def wrap(_monoid, %Lens{} = lens) do
LensCompose.new(lens)
end
@doc """
Extracts the lens from the LensCompose wrapper.
"""
def unwrap(%LensCompose{lens: lens}), do: lens
end