lib/witchcraft/semigroup.ex

import TypeClass

defclass Witchcraft.Semigroup do
  @moduledoc ~S"""
  A semigroup is a structure describing data that can be appendenated with others of its type.
  That is to say that appending another list returns a list, appending one map
  to another returns a map, and appending two integers returns an integer, and so on.

  These can be chained together an arbitrary number of times. For example:

      1 <> 2 <> 3 <> 5 <> 7 == 18
      [1, 2, 3] <> [4, 5, 6] <> [7, 8, 9] == [1, 2, 3, 4, 5, 6, 7, 8, 9]
      "foo" <> " " <> "bar" == "foo bar"

  This generalizes the idea of a monoid, as it does not require an `empty` version.

  ## Type Class

  An instance of `Witchcraft.Semigroup` must define `Witchcraft.Semigroup.append/2`.

      Semigroup  [append/2]
  """

  alias __MODULE__

  use Witchcraft.Internal, overrides: [<>: 2]

  @type t :: any()

  where do
    @doc ~S"""
    `append`enate two data of the same type. These can be chained together an arbitrary number of times. For example:

        iex> 1 |> append(2) |> append(3)
        6

        iex> [1, 2, 3]
        ...> |> append([4, 5, 6])
        ...> |> append([7, 8, 9])
        [1, 2, 3, 4, 5, 6, 7, 8, 9]

        iex> "foo" |> append(" ") |> append("bar")
        "foo bar"

    ## Operator

        iex> use Witchcraft.Semigroup
        ...> 1 <> 2 <> 3 <> 5 <> 7
        18

        iex> use Witchcraft.Semigroup
        ...> [1, 2, 3] <> [4, 5, 6] <> [7, 8, 9]
        [1, 2, 3, 4, 5, 6, 7, 8, 9]

        iex> use Witchcraft.Semigroup
        ...> "foo" <> " " <> "bar"
        "foo bar"

    There is an operator alias `a <> b`. Since this conflicts with `Kernel.<>/2`,
    `use Witchcraft,Semigroup` will automatically exclude the Kernel operator.
    This is highly recommended, since `<>` behaves the same on bitstrings, but is
    now available on more datatypes.

    """
    def append(a, b)
  end

  defalias a <> b, as: :append

  @doc ~S"""
  Flatten a list of homogeneous semigroups to a single container.

  ## Example

      iex> concat [
      ...>   [1, 2, 3],
      ...>   [4, 5, 6]
      ...> ]
      [1, 2, 3, 4, 5, 6]

  """
  @spec concat(Semigroup.t()) :: [Semigroup.t()]
  def concat(semigroup_of_lists) do
    Enum.reduce(semigroup_of_lists, [], &Semigroup.append(&2, &1))
  end

  @doc ~S"""
  Repeat the contents of a semigroup a certain number of times.

  ## Examples

      iex> [1, 2, 3] |> repeat(times: 3)
      [1, 2, 3, 1, 2, 3, 1, 2, 3]

  """
  @spec repeat(Semigroup.t(), times: non_neg_integer()) :: Semigroup.t()
  # credo:disable-for-lines:6 Credo.Check.Refactor.PipeChainStart
  def repeat(to_repeat, times: times) do
    fn -> to_repeat end
    |> Stream.repeatedly()
    |> Stream.take(times)
    |> Enum.reduce(&Semigroup.append(&2, &1))
  end

  properties do
    def associative(data) do
      a = generate(data)
      b = generate(data)
      c = generate(data)

      left = a |> Semigroup.append(b) |> Semigroup.append(c)
      right = Semigroup.append(a, Semigroup.append(b, c))

      equal?(left, right)
    end
  end
end

definst Witchcraft.Semigroup, for: Function do
  def append(f, g) when is_function(g), do: Quark.compose(g, f)
end

definst Witchcraft.Semigroup, for: Witchcraft.Unit do
  def append(_, _), do: %Witchcraft.Unit{}
end

definst Witchcraft.Semigroup, for: Integer do
  def append(a, b), do: a + b
end

definst Witchcraft.Semigroup, for: Float do
  def append(a, b), do: a + b
end

definst Witchcraft.Semigroup, for: BitString do
  def append(a, b), do: Kernel.<>(a, b)
end

definst Witchcraft.Semigroup, for: List do
  def append(a, b), do: a ++ b
end

definst Witchcraft.Semigroup, for: Map do
  def append(a, b), do: Map.merge(a, b)
end

definst Witchcraft.Semigroup, for: MapSet do
  def append(a, b), do: MapSet.union(a, b)
end

definst Witchcraft.Semigroup, for: Tuple do
  # credo:disable-for-lines:5 Credo.Check.Refactor.PipeChainStart
  custom_generator(_) do
    Stream.repeatedly(fn -> TypeClass.Property.Generator.generate(%{}) end)
    |> Enum.take(10)
    |> List.to_tuple()
  end

  def append(tuple_a, tuple_b) do
    tuple_a
    |> Tuple.to_list()
    |> Enum.zip(Tuple.to_list(tuple_b))
    |> Enum.map(fn {x, y} -> Witchcraft.Semigroup.append(x, y) end)
    |> List.to_tuple()
  end
end