lib/witchcraft/comonad.ex

import TypeClass

defclass Witchcraft.Comonad do
  @moduledoc """
  The dual of monads, `Comonad` brings an unwrapping function to `Extend`able data.

  Note that the unwrapping function (`extract`) *must return a value*, and is not
  available on many data structres that have an empty element. For example,
  there is no `Comonad` instance for `List` because we cannot pull a value
  out of `[]`.

  ## Type Class

  An instance of `Witchcraft.Comonad` must also implement `Witchcraft.Extend`,
  and define `Witchcraft.Comonad.extract/1`.

      Functor  [map/2]
         ↓
      Extend   [nest/1]
         ↓
      Comonad  [extract/1]
  """

  alias __MODULE__

  extend Witchcraft.Extend

  use Witchcraft.Internal, deps: [Witchcraft.Extend]

  use Quark

  @type t :: any()

  where do
    @doc """
    Extract a value out of some context / data structure. This is the opposite
    of `Witchcraft.Applicative.of/2`.

    ## Examples

        iex> extract({1, 2})
        2

        extract(%Id{id: 42})
        #=> 42

    """
    @spec extract(Comonad.t()) :: any()
    def extract(nested)
  end

  @doc """
  Alias for `extract/1`

  ## Examples

      iex> unwrap({1, 2})
      2

      unwrap(%Id{id: 42})
      #=> 42

  """
  @spec unwrap(Comonad.t()) :: any()
  def unwrap(nested), do: extract(nested)

  properties do
    def left_identity(data) do
      a = generate(data)

      a
      |> Witchcraft.Extend.extend(&Comonad.extract/1)
      |> equal?(a)
    end

    def right_identity(data) do
      a = generate(data)

      f = fn x ->
        x
        |> Comonad.extract()
        |> inspect()
      end

      a
      |> Witchcraft.Extend.extend(f)
      |> Comonad.extract()
      |> equal?(f.(a))
    end
  end
end

definst Witchcraft.Comonad, for: Tuple do
  custom_generator(_) do
    import TypeClass.Property.Generator, only: [generate: 1]
    {generate(nil), generate(nil)}
  end

  def extract(tuple) when tuple_size(tuple) >= 1 do
    elem(tuple, tuple_size(tuple) - 1)
  end
end