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