Packages

A functional optics library. Create and compose lenses to view, set, and modify data inside arbitrarily nested maps, lists, and tuples.

Current section

Files

Jump to
focus lib focus.ex
Raw

lib/focus.ex

defprotocol Focusable do
@doc "View the data that an optic focuses on."
def view(optic, structure)
@doc "Modify the data that an optic focuses on."
def over(optic, structure, f)
@doc "Set the data that an optic focuses on."
def set(optic, structure, value)
end
defmodule Focus do
alias Focus.Types
@moduledoc "Common functions usable by lenses, prisms, and traversals."
@doc "Wrapper around Focusable.view/2"
@spec view(Types.optic, Types.traversable) :: any | nil
def view(optic, structure) do
Focusable.view(optic, structure)
end
@doc "Wrapper around Focusable.over/3"
@spec over(Types.optic, Types.traversable, ((any) -> any)) :: Types.traversable
def over(optic, structure, f) do
Focusable.over(optic, structure, f)
end
@doc "Wrapper around Focusable.set/3"
def set(optic, structure, v) do
Focusable.set(optic, structure, v)
end
@doc """
Compose with most general lens on the left
## Examples
iex> marge = %{
...> name: "Marge",
...> address: %{
...> street: "123 Fake St.",
...> city: "Springfield"
...> }
...> }
iex> address_lens = Lens.make_lens(:address)
iex> street_lens = Lens.make_lens(:street)
iex> composed = Focus.compose(address_lens, street_lens)
iex> Focus.view(composed, marge)
"123 Fake St."
"""
@spec compose(Types.optic, Types.optic) :: Types.optic
def compose(%{get: get_x, put: set_x}, %{get: get_y, put: set_y}) do
%Lens{
get: fn s ->
case get_x.(s) do
{:error, {:lens, :bad_path}} ->
{:error, {:lens, :bad_path}}
x ->
get_y.(x)
end
end,
put: fn s ->
fn f ->
case get_x.(s) do
{:error, {:lens, :bad_path}} ->
{:error, {:lens, :bad_path}}
x ->
set_x.(s).(set_y.(x).(f))
end
end
end
}
end
@doc """
Infix lens composition
## Examples
iex> import Focus
iex> marge = %{name: "Marge", address: %{
...> local: %{number: 123, street: "Fake St."},
...> city: "Springfield"}
...> }
iex> address_lens = Lens.make_lens(:address)
iex> local_lens = Lens.make_lens(:local)
iex> street_lens = Lens.make_lens(:street)
iex> address_lens ~> local_lens ~> street_lens |> Focus.view(marge)
"Fake St."
"""
@spec (Types.optic) ~> (Types.optic) :: Types.optic
def x ~> y do
compose(x, y)
end
@doc """
Compose a pair of lenses to operate at the same level as one another.
Calling Focus.view/2, Focus.over/3, or Focus.set/3 on an alongside composed
pair returns a two-element tuple of the result.
## Examples
iex> nums = [1,2,3,4,5,6]
iex> Focus.alongside(Lens.idx(0), Lens.idx(3))
...> |> Focus.view(nums)
{1, 4}
iex> bart = %{name: "Bart", parents: {"Homer", "Marge"}, age: 10}
iex> Focus.alongside(Lens.make_lens(:name), Lens.make_lens(:age))
...> |> Focus.view(bart)
{"Bart", 10}
"""
@spec alongside(Types.optic, Types.optic) :: Types.optic
def alongside(%{get: get_x, put: set_x}, %{get: get_y, put: set_y}) do
%Lens{
get: fn s ->
{get_x.(s), get_y.(s)}
end,
put: fn s ->
fn f ->
{set_x.(s).(f), set_y.(s).(f)}
end
end
}
end
@doc """
Given a list of lenses and a structure, apply Focus.view/2 for each lens
to the structure.
## Examples
iex> homer = %{
...> name: "Homer",
...> job: "Nuclear Safety Inspector",
...> children: ["Bart", "Lisa", "Maggie"]
...> }
iex> lenses = Lens.make_lenses(homer)
iex> [lenses.name, lenses.children]
...> |> Focus.view_list(homer)
["Homer", ["Bart", "Lisa", "Maggie"]]
"""
@spec view_list(list(Types.optic), Types.traversable) :: [any]
def view_list(lenses, structure) when is_list(lenses) do
for lens <- lenses do
Focus.view(lens, structure)
end
end
@doc """
Check whether an optic's target is present in a data structure.
## Examples
iex> first_elem = Lens.idx(1)
iex> first_elem |> Focus.has([0])
false
iex> name = Lens.make_lens(:name)
iex> name |> Focus.has(%{name: "Homer"})
true
"""
@spec has(Types.optic, Types.traversable) :: boolean
def has(optic, structure) do
case Focus.view(optic, structure) do
nil -> false
{:error, _} -> false
_ -> true
end
end
@doc """
Check whether an optic's target is not present in a data structure.
## Examples
iex> first_elem = Lens.idx(1)
iex> first_elem |> Focus.hasnt([0])
true
iex> name = Lens.make_lens(:name)
iex> name |> Focus.hasnt(%{name: "Homer"})
false
"""
@spec hasnt(Types.optic, Types.traversable) :: boolean
def hasnt(optic, structure), do: !has(optic, structure)
@doc """
Partially apply a lens to Focus.over/3, fixing the lens argument and
returning a function that takes a Types.traversable and an update function.
## Examples
iex> upcase_name = Lens.make_lens(:name)
...> |> Focus.fix_over(&String.upcase/1)
iex> %{name: "Bart", parents: {"Homer", "Marge"}}
...> |> upcase_name.()
%{name: "BART", parents: {"Homer", "Marge"}}
iex> fst = Lens.idx(0)
iex> states = [:maryland, :texas, :illinois]
iex> Focus.over(fst, states, &String.upcase(Atom.to_string(&1)))
["MARYLAND", :texas, :illinois]
"""
@spec fix_over(Types.optic, ((any) -> any)) :: ((Types.traversable) -> Types.traversable)
def fix_over(%{get: _, put: _} = lens, f \\ fn x -> x end) when is_function(f) do
fn structure ->
Focus.over(lens, structure, f)
end
end
@doc """
Partially apply a lens to Focus.set/3, fixing the optic argument and
returning a function that takes a Types.traversable and a new value.
## Examples
iex> name_setter = Lens.make_lens(:name)
...> |> Focus.fix_set
iex> %{name: "Bart", parents: {"Homer", "Marge"}}
...> |> name_setter.("Lisa")
%{name: "Lisa", parents: {"Homer", "Marge"}}
iex> fst = Lens.idx(0)
iex> states = [:maryland, :texas, :illinois]
iex> Focus.over(fst, states, &String.upcase(Atom.to_string(&1)))
["MARYLAND", :texas, :illinois]
"""
@spec fix_set(Types.optic) :: ((Types.traversable, any) -> Types.traversable)
def fix_set(%{get: _, put: _} = lens) do
fn structure, val ->
Focus.set(lens, structure, val)
end
end
@doc """
Fix Focus.view/2 on a given optic. This partially applies Focus.view/2 with the given
optic and returns a function that takes a Types.traversable structure.
## Examples
iex> view_name = Lens.make_lens(:name)
...> |> Focus.fix_view
iex> homer = %{name: "Homer"}
iex> view_name.(homer)
"Homer"
iex> [homer, %{name: "Marge"}, %{name: "Bart"}]
...> |> Enum.map(&view_name.(&1))
["Homer", "Marge", "Bart"]
"""
@spec fix_view(Types.optic) :: (Types.traversable -> any)
def fix_view(%{get: _, put: _} = optic) do
fn structure ->
Focus.view(optic, structure)
end
end
end