import TypeClass
defclass Witchcraft.Semigroupoid do
@moduledoc """
A semigroupoid describes some way of composing morphisms on between some
collection of objects.
## Type Class
An instance of `Witchcraft.Semigroupoid` must define `Witchcraft.Semigroupoid.compose/2`.
Semigroupoid [compose/2]
"""
alias __MODULE__
use Witchcraft.Internal, overrides: [apply: 2]
@type t :: any()
where do
@doc """
Take two morphisms and return their composition "the math way".
That is, `(b -> c) -> (a -> b) -> (a -> c)`.
## Examples
iex> times_ten_plus_one = compose(fn x -> x + 1 end, fn y -> y * 10 end)
...> times_ten_plus_one.(5)
51
"""
@spec compose(Semigroupoid.t(), Semigroupoid.t()) :: Semigroupoid.t()
def compose(morphism_a, morphism_b)
@doc """
Express how to apply arguments to the _very end_ of a semigroupoid,
or "run the morphism". This should not be used to inject values part way
though a composition chain.
It is provided here to remain idiomatic with Elixir, and to make
prop testing _possible_.
## Examples
iex> Witchcraft.Semigroupoid.apply(&inspect/1, [42])
"42"
"""
@spec apply(Semigroupoid.t(), [any()]) :: Semigroupoid.t() | any()
def apply(morphism, arguments)
end
@doc """
Pipe some data through a morphism.
Similar to `apply/2`, but with a single argument, not needing to wrap
the argument in a list.
## Examples
iex> pipe(42, &(&1 + 1))
43
"""
@spec pipe(any(), Semigroupoid.t()) :: any()
def pipe(data, fun), do: apply(fun, [data])
@doc """
`compose/2`, but with the arguments flipped (same direction as `|>`).
## Examples
iex> times_ten_plus_one = pipe_compose(fn y -> y * 10 end, fn x -> x + 1 end)
...> times_ten_plus_one.(5)
51
"""
@spec pipe_compose(t(), t()) :: t()
def pipe_compose(b, a), do: compose(a, b)
@doc """
Composition operator "the math way". Alias for `compose/2`.
## Examples
iex> times_ten_plus_one =
...> fn x -> x + 1 end
...> <|> fn y -> y * 10 end
...>
...> times_ten_plus_one.(5)
51
"""
@spec t() <|> any() :: t()
def g <|> f, do: compose(g, f)
@doc """
Composition operator "the pipe way". Alias for `pipe_compose/2`.
## Examples
iex> times_ten_plus_one =
...> fn y -> y * 10 end
...> <~> fn x -> x + 1 end
...>
...> times_ten_plus_one.(5)
51
"""
@spec t() <~> any() :: t()
def f <~> g, do: compose(g, f)
properties do
def associativity(data) do
a = generate(data)
b = generate(data)
c = generate(data)
left = Semigroupoid.compose(Semigroupoid.compose(a, b), c)
right = Semigroupoid.compose(a, Semigroupoid.compose(b, c))
equal?(left, right)
end
end
end
definst Witchcraft.Semigroupoid, for: Function do
def apply(fun, args), do: Kernel.apply(fun, args)
def compose(fun_a, fun_b), do: Quark.compose(fun_a, fun_b)
end