lib/witchcraft/bifunctor.ex

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