import TypeClass defclass Witchcraft.Bifunctor do @moduledoc """ Similar to `Witchcraft.Functor`, but able to map two functions over two separate portions of some data structure (some product type). Especially helpful when you need different hebaviours on different fields. ## Type Class An instance of `Witchcraft.Bifunctor` must also implement `Witchcraft.Functor`, and define `Witchcraft.Apply.ap/2`. Functor [map/2] ↓ Bifunctor [bimap/2] """ alias __MODULE__ extend Witchcraft.Functor use Witchcraft.Internal use Quark @type t :: any() where do @doc """ `map` separate fuctions over two fields in a product type. The order of fields doesn't always matter in the map. The first/second function application is determined by the instance. It also does not have to map all fields in a product type. ## Diagram ┌------------------------------------┐ ↓ | %Combo{a: 5, b: :ok, c: "hello"} |> bimap(&(&1 * 100), &String.upcase/1) ↑ | └---------------------------------┘ #=> %Combo{a: 500, b: :ok, c: "HELLO"} ## Examples iex> {1, "a"} |> bimap(&(&1 * 100), &(&1 <> "!")) {100, "a!"} iex> {:msg, 42, "number is below 50"} ...> |> bimap(&(%{subject: &1}), &String.upcase/1) {:msg, %{subject: 42}, "NUMBER IS BELOW 50"} """ @spec bimap(Bifunctor.t(), (any() -> any()), (any() -> any())) :: Bifunctor.t() def bimap(data, f, g) end properties do def identity(data) do a = generate(data) left = Bifunctor.bimap(a, &Quark.id/1, &Quark.id/1) equal?(left, a) end def composition(data) do a = generate(data) f = &Witchcraft.Semigroup.append(&1, &1) g = &inspect/1 h = &is_number/1 i = &!/1 left = Bifunctor.bimap(a, fn x -> f.(g.(x)) end, fn y -> h.(i.(y)) end) right = a |> Bifunctor.bimap(g, i) |> Bifunctor.bimap(f, h) equal?(left, right) end end @doc """ The same as `bimap/3`, but with the functions curried ## Examples iex> {:ok, 2, "hi"} ...> |> bilift(&*/2, &<>/2) ...> |> bimap(fn f -> f.(9) end, fn g -> g.("?!") end) {:ok, 18, "hi?!"} """ @spec bilift(Bifunctor.t(), fun(), fun()) :: Bifunctor.t() def bilift(data, f, g), do: bimap(data, curry(f), curry(g)) @doc """ `map` a function over the first value only ## Examples iex> {:ok, 2, "hi"} |> map_first(&(&1 * 100)) {:ok, 200, "hi"} """ @spec map_first(Bifunctor.t(), (any() -> any())) :: Bifunctor.t() def map_first(data, f), do: Bifunctor.bimap(data, f, &Quark.id/1) @doc """ The same as `map_first`, but with a curried function ## Examples iex> {:ok, 2, "hi"} ...> |> lift_first(&*/2) ...> |> map_first(fn f -> f.(9) end) {:ok, 18, "hi"} """ @spec lift_first(Bifunctor.t(), fun()) :: Bifunctor.t() def lift_first(data, f), do: map_first(data, curry(f)) @doc """ `map` a function over the second value only ## Examples iex> {:ok, 2, "hi"} |> map_second(&(&1 <> "!?")) {:ok, 2, "hi!?"} """ @spec map_second(Bifunctor.t(), (any() -> any())) :: Bifunctor.t() def map_second(data, g), do: Bifunctor.bimap(data, &Quark.id/1, g) @doc """ The same as `map_second`, but with a curried function ## Examples iex> {:ok, 2, "hi"} ...> |> lift_second(&<>/2) ...> |> map_second(fn f -> f.("?!") end) {:ok, 2, "hi?!"} """ @spec lift_second(Bifunctor.t(), fun()) :: Bifunctor.t() def lift_second(data, g), do: map_second(data, curry(g)) end definst Witchcraft.Bifunctor, for: Tuple do # credo:disable-for-lines:6 Credo.Check.Refactor.PipeChainStart custom_generator(_) do fn -> TypeClass.Property.Generator.generate(nil) end |> Stream.repeatedly() |> Enum.take(Enum.random(2..12)) |> List.to_tuple() end def bimap(tuple, f, g) do case tuple do {a, b} -> {f.(a), g.(b)} {x, a, b} -> {x, f.(a), g.(b)} {x, y, a, b} -> {x, y, f.(a), g.(b)} {x, y, z, a, b} -> {x, y, z, f.(a), g.(b)} big_tuple when tuple_size(big_tuple) > 5 -> index_a = tuple_size(big_tuple) - 2 mapped_a = big_tuple |> elem(index_a) |> f.() big_tuple |> Witchcraft.Functor.map(g) |> put_elem(index_a, mapped_a) end end end