defmodule Focus.Lens do alias Focus.Lens @moduledoc """ Experimenting with functional lenses. """ @enforce_keys [:getter, :setter] defstruct [:getter, :setter] @type t :: %Lens{ getter: ((any) -> any), setter: (((any) -> any) -> any) } @type traversable :: map | list | tuple @doc """ Define a lens to focus on a part of a data structure. ## Examples iex> alias Focus.Lens iex> person = %{name: "Homer"} iex> nameLens = Lens.makeLens(:name) iex> nameLens.getter.(person) "Homer" iex> nameLens.setter.(person).("Bart") %{name: "Bart"} """ @spec makeLens(list) :: Lens.t def makeLens(path) do %Lens{ getter: fn s -> getter(s, path) end, setter: fn s -> fn f -> setter(s, path, f) end end } end defp getter(%{__struct__: _} = s, x), do: Map.get(s, x) defp getter(s, x) when is_map(s), do: Access.get(s, x) defp getter(s, x) when is_list(s), do: get_in(s, [Access.at(x)]) defp getter(s, x) when is_tuple(s), do: elem(s, x) defp setter(s, x, f) when is_map(s), do: Map.put(s, x, f) defp setter(s, x, f) when is_list(s), do: List.replace_at(s, x, f) defp setter(s, x, f) when is_tuple(s) do s |> Tuple.delete_at(x) |> Tuple.insert_at(x, f) end @doc """ Compose with most general lens on the left ## Examples iex> alias Focus.Lens iex> marge = %{name: "Marge", address: %{street: "123 Fake St.", city: "Springfield"}} iex> addressLens = Lens.makeLens(:address) iex> streetLens = Lens.makeLens(:street) iex> composed = Lens.compose(addressLens, streetLens) iex> Lens.view(composed, marge) {:ok, "123 Fake St."} """ def compose(%Lens{getter: get_x, setter: set_x}, %Lens{getter: get_y, setter: set_y}) do %Lens{ getter: fn s -> get_y.(get_x.(s)) end, setter: fn s -> fn f -> set_x.(s).(set_y.(get_x.(s)).(f)) end end } end @doc """ Infix lens composition ## Examples iex> import Focus.Lens iex> alias Focus.Lens iex> marge = %{name: "Marge", address: %{ ...> local: %{number: 123, street: "Fake St."}, ...> city: "Springfield"} ...> } iex> addressLens = Lens.makeLens(:address) iex> localLens = Lens.makeLens(:local) iex> streetLens = Lens.makeLens(:street) iex> addressLens ~> localLens ~> streetLens |> Lens.view!(marge) "Fake St." """ def x ~> y do Lens.compose(x, y) end @doc """ Get a piece of a data structure that a lens focuses on. ## Examples iex> alias Focus.Lens iex> marge = %{name: "Marge", address: %{street: "123 Fake St.", city: "Springfield"}} iex> nameLens = Lens.makeLens(:name) iex> Lens.view!(nameLens, marge) "Marge" """ @spec view!(Lens.t, traversable) :: any | nil def view!(%Lens{getter: getter}, structure) do getter.(structure) end @doc """ Get a piece of a data structure that a lens focuses on; returns {:ok, data} | {:error, :bad_lens_path} ## Examples iex> alias Focus.Lens iex> marge = %{name: "Marge", address: %{street: "123 Fake St.", city: "Springfield"}} iex> nameLens = Lens.makeLens(:name) iex> Lens.view(nameLens, marge) {:ok, "Marge"} """ @spec view(Lens.t, traversable) :: {:error, :bad_arg} | {:ok, any} def view(%Lens{} = lens, structure) do res = view!(lens, structure) case res do nil -> {:error, :bad_lens_path} _ -> {:ok, res} end end @doc """ Modify the part of a data structure that a lens focuses on. ## Examples iex> alias Focus.Lens iex> marge = %{name: "Marge", address: %{street: "123 Fake St.", city: "Springfield"}} iex> nameLens = Lens.makeLens(:name) iex> Lens.over(nameLens, marge, &String.upcase/1) %{name: "MARGE", address: %{street: "123 Fake St.", city: "Springfield"}} """ @spec over(Lens.t, traversable, (any -> any)) :: traversable def over(%Lens{setter: setter} = lens, structure, f) do with {:ok, d} <- Lens.view(lens, structure) do setter.(structure).(f.(d)) end end @doc """ Update the part of a data structure the lens focuses on. ## Examples iex> alias Focus.Lens iex> marge = %{name: "Marge", address: %{street: "123 Fake St.", city: "Springfield"}} iex> nameLens = Lens.makeLens(:name) iex> Lens.set(nameLens, marge, "Homer") %{name: "Homer", address: %{street: "123 Fake St.", city: "Springfield"}} iex> alias Focus.Lens iex> marge = %{name: "Marge", address: %{street: "123 Fake St.", city: "Springfield"}} iex> addressLens = Lens.makeLens(:address) iex> streetLens = Lens.makeLens(:street) iex> composed = Lens.compose(addressLens, streetLens) iex> Lens.set(composed, marge, "42 Wallaby Way") %{name: "Marge", address: %{street: "42 Wallaby Way", city: "Springfield"}} """ @spec set(Lens.t, traversable, any) :: traversable def set(%Lens{setter: setter} = lens, structure, val) do with {:ok, _d} <- Lens.view(lens, structure) do setter.(structure).(val) end end @doc """ Given a list of lenses and a structure, apply Lens.view for each lens to the structure. ## Examples iex> homer = %{name: "Homer", job: "Nuclear Safety Inspector", children: ["Bart", "Lisa", "Maggie"]} iex> lenses = [Focus.Lens.makeLens(:name), Focus.Lens.makeLens(:children)] iex> Focus.Lens.apply_list(lenses, homer) ["Homer", ["Bart", "Lisa", "Maggie"]] """ @spec apply_list(list(Lens.t), traversable) :: [any] def apply_list(lenses, structure) when is_list(lenses) do for lens <- lenses do Lens.view!(lens, structure) end end end