defmodule Lens do alias Focus.Types @moduledoc """ Lenses combine getters and setters for keys in data structures. Lenses should match/operate over a single value in a data structure, e.g. a key in a map/struct. """ @enforce_keys [:get, :put] defstruct [:get, :put] @type t :: %Lens{ get: ((any) -> any), put: (((any) -> any) -> any) } @doc """ Define a lens to Focus on a part of a data structure. ## Examples iex> person = %{name: "Homer"} iex> name_lens = Lens.make_lens(:name) iex> name_lens |> Focus.view(person) "Homer" iex> name_lens |> Focus.set(person, "Bart") %{name: "Bart"} """ @spec make_lens(any) :: Lens.t def make_lens(path) do %Lens{ get: fn s -> Lensable.getter(s, path) end, put: fn s -> fn f -> Lensable.setter(s, path, f) end end } end @doc """ Automatically generate the valid lenses for the supplied map-like data structure. ## Examples iex> lisa = %{name: "Lisa", pets: %{cat: "Snowball"}} iex> lisa_lenses = Lens.make_lenses(lisa) iex> lisa_lenses.name ...> |> Focus.view(lisa) "Lisa" iex> pet_lenses = Lens.make_lenses(lisa.pets) iex> lisa_lenses.pets ...> ~> pet_lenses.cat ...> |> Focus.set(lisa, "Snowball II") %{name: "Lisa", pets: %{cat: "Snowball II"}} """ @spec make_lenses(Types.traversable) :: %{optional(atom) => Lens.t, optional(String.t) => Lens.t} def make_lenses(%{} = structure) when is_map(structure) do for key <- Map.keys(structure), into: %{} do {key, Lens.make_lens(key)} end end @doc """ Define a struct and derive lenses for the struct's keys as functions in the module. Examples assume the following module: ```elixir defmodule PersonExample do import Lens deflenses name: nil, age: nil end ``` ## Example iex> function_exported?(PersonExample, :age_lens, 0) true iex> function_exported?(PersonExample, :name_lens, 0) true iex> bart = %PersonExample{name: "Bart", age: 10} iex> PersonExample.name_lens |> Focus.view(bart) "Bart" end """ defmacro deflenses(fields) do quote do Module.register_attribute(__MODULE__, :struct_fields, accumulate: true) for field <- unquote(fields) do Module.put_attribute(__MODULE__, :struct_fields, field) end Module.eval_quoted(__ENV__, [ Lens.__defstruct__(@struct_fields), Lens.__deflenses__(@struct_fields) ] ) end end @doc false def __deflenses__(fields) do if Keyword.keyword?(fields) do for {field, _val} <- fields do quote do def unquote(:"#{field}_lens")() do Lens.make_lens(unquote(field)) end end end else for field <- fields do quote do def unquote(:"#{field}_lens")() do Lens.make_lens(unquote(field)) end end end end end @doc false def __defstruct__(fields) do quote do defstruct unquote(Macro.escape(fields)) defimpl Lensable, for: __MODULE__ do def getter(s, x), do: Map.get(s, x) def setter(s, x, f), do: Map.put(s, x, f) end end end @doc """ A lens that focuses on an index in a list. ## Examples iex> first_elem = Lens.idx(0) iex> first_elem |> Focus.view([1,2,3,4,5]) 1 iex> bad_index = Lens.idx(10) iex> bad_index |> Focus.view([1,2,3]) nil """ @spec idx(number) :: Lens.t def idx(num) when is_number(num), do: make_lens(num) defimpl Focusable do @doc """ View the data that an optic Focuses on. ## Examples iex> marge = %{ ...> name: "Marge", ...> address: %{ ...> street: "123 Fake St.", ...> city: "Springfield" ...> } ...> } iex> name_lens = Lens.make_lens(:name) iex> Focus.view(name_lens, marge) "Marge" """ @spec view(Lens.t, Types.traversable) :: any | nil def view(%Lens{get: get}, structure), do: get.(structure) @doc """ Modify the part of a data structure that a lens Focuses on. ## Examples iex> marge = %{name: "Marge", address: %{street: "123 Fake St.", city: "Springfield"}} iex> name_lens = Lens.make_lens(:name) iex> Focus.over(name_lens, marge, &String.upcase/1) %{name: "MARGE", address: %{street: "123 Fake St.", city: "Springfield"}} """ @spec over(Lens.t, Types.traversable, (any -> any)) :: Types.traversable def over(%Lens{put: setter} = lens, structure, f) do with {:ok, d} <- Lens.safe_view(lens, structure) do setter.(structure).(f.(d)) end end @doc """ Update the part of a data structure the lens Focuses on. ## Examples iex> marge = %{name: "Marge", address: %{street: "123 Fake St.", city: "Springfield"}} iex> name_lens = Lens.make_lens(:name) iex> Focus.set(name_lens, marge, "Homer") %{name: "Homer", address: %{street: "123 Fake St.", city: "Springfield"}} 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.set(composed, marge, "42 Wallaby Way") %{name: "Marge", address: %{street: "42 Wallaby Way", city: "Springfield"}} """ @spec set(Lens.t, Types.traversable, any) :: Types.traversable def set(lens, structure, val) do over(lens, structure, fn _ -> val end) end end @doc """ Get a piece of a data structure that a lens Focuses on; returns {:ok, data} | {:error, :bad_lens_path} | {:error, :bad_data_structure} ## Examples iex> marge = %{name: "Marge", address: %{street: "123 Fake St.", city: "Springfield"}} iex> name_lens = Lens.make_lens(:name) iex> Lens.safe_view(name_lens, marge) {:ok, "Marge"} """ @spec safe_view(Lens.t, Types.traversable) :: {:error, {:lens, :bad_path}} | {:error, {:lens, :bad_data_structure}} | {:ok, any} def safe_view(%Lens{} = lens, structure) do res = Focus.view(lens, structure) case res do {:error, err} -> {:error, err} _ -> {:ok, res} end end end