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