import TypeClass defclass Witchcraft.Extend do @moduledoc """ `Extend` is essentially "co`Chain`", meaning that it reverses the relationships in `Chain`. Instead of a flattening operation, we have `nest` which wraps the data in an additional layer of itsef. Instead of a `chain`ing function that acts on raw data and wraps it, we have `extend` which unwraps data, may modify it, and returns the unwrapped value ## Type Class An instance of `Witchcraft.Extend` must also implement `Witchcraft.Functor`, and define `Witchcraft.Extend.nest/1`. Functor [map/2] ↓ Extend [nest/1] """ alias __MODULE__ alias Witchcraft.Functor extend Witchcraft.Functor use Witchcraft.Internal, deps: [Witchcraft.Functor] use Quark @type t :: any() @type colink :: (Extend.t() -> any()) where do @doc """ Wrap some nestable data structure in another layer of itself ## Examples iex> nest([1, 2, 3]) [[1, 2, 3], [2, 3], [3]] """ @spec nest(Extend.t()) :: Extend.t() def nest(data) end properties do def extend_composition(data) do if is_function(data) do use Witchcraft.Semigroup a = &inspect/1 monoid = Enum.random([1, [], ""]) arg1 = generate(monoid) arg2 = generate(monoid) arg3 = generate(monoid) f = fn x -> compose(x, fn a -> a <> a end) end g = fn y -> compose(y, fn b -> b <> b <> b end) end left = a |> Witchcraft.Extend.extend(g) |> Witchcraft.Extend.extend(f) right = Witchcraft.Extend.curried_extend(a, fn x -> x |> Witchcraft.Extend.curried_extend(g) |> f.() end) equal?(left.(arg1).(arg2).(arg3), right.(arg1).(arg2).(arg3)) else a = generate(data) f = fn x -> "#{inspect(x)}-#{inspect(x)}" end g = fn y -> "#{inspect(y)} / #{inspect(y)} / #{inspect(y)}" end left = a |> Witchcraft.Extend.curried_extend(g) |> Witchcraft.Extend.curried_extend(f) right = Witchcraft.Extend.curried_extend(a, fn x -> x |> Witchcraft.Extend.curried_extend(g) |> f.() end) equal?(left, right) end end def naturality(data) do a = generate(data) if is_function(data) do fun = &inspect/1 monoid = Enum.random([1, [], ""]) arg1 = generate(monoid) arg2 = generate(monoid) arg3 = generate(monoid) left = fun |> Extend.nest() |> Functor.lift(&Extend.nest/1) right = fun |> Extend.nest() |> Extend.nest() equal?(left.(arg1).(arg2).(arg3), right.(arg1).(arg2).(arg3)) else a |> Extend.nest() |> Functor.lift(&Extend.nest/1) |> equal?(a |> Extend.nest() |> Extend.nest()) end end def extend_as_nest(data) do if is_function(data) do fun = &inspect/1 monoid = Enum.random([1, [], ""]) arg1 = generate(monoid) arg2 = generate(monoid) left = Witchcraft.Extend.extend(fun, &Quark.id/1) right = Witchcraft.Extend.nest(fun) equal?(left.(arg1).(arg2), right.(arg1).(arg2)) true else a = generate(data) a |> Witchcraft.Extend.extend(&Quark.id/1) |> equal?(Witchcraft.Extend.nest(a)) end end def nest_as_extend(data) do if is_function(data) do f = fn x -> x <> x end g = &inspect/1 monoid = Enum.random([1, [], ""]) arg1 = generate(monoid) arg2 = generate(monoid) left = g |> Extend.nest() |> Functor.lift(&Functor.lift(&1, f)) right = Extend.nest(Functor.lift(g, f)) equal?(left.(arg1).(arg2), right.(arg1).(arg2)) else a = generate(data) f = &inspect/1 a |> Extend.nest() |> Functor.lift(&Functor.lift(&1, f)) |> equal?(Extend.nest(Functor.lift(a, f))) end end end @doc """ Similar to `Witchcraft.Chain.chain/2`, except that it reverses the input and output types of the colinking function. ## Examples Chain: iex> Witchcraft.Chain.chain([1, 2, 3], fn x -> [x * 10] end) [10, 20, 30] Extend: iex> extend([1, 2, 3], fn list -> List.first(list) * 10 end) [10, 20, 30] """ @spec extend(Extend.t(), Extend.colink()) :: Extend.t() def extend(data, colink) do data |> nest() |> Functor.map(colink) end @doc """ `extend/2` with arguments flipped. Makes piping composed colinks easier (see `compose_colink/2` and `pipe_compose_colink/2`). ## Examples iex> fn list -> List.first(list) * 10 end ...> |> peel([1, 2, 3]) [10, 20, 30] """ @spec peel(Extend.colink(), Extend.t()) :: Extend.t() def peel(colink, data), do: Extend.extend(data, colink) @doc """ The same as `extend/2`, but with the colinking function curried. ## Examples iex> [1, 2, 3] ...> |> curried_extend(fn(list, coeff) -> List.first(list) * coeff end) ...> |> extend(fn(funs) -> List.first(funs).(10) end) [10, 20, 30] """ @spec curried_extend(Extend.t(), fun()) :: Extend.t() def curried_extend(data, colink), do: Extend.extend(data, curry(colink)) @doc """ The same as `extend/2`, but with the colinking function curried. ## Examples iex> fn(list) -> List.first(list) * 10 end ...> |> curried_peel([1, 2, 3]) [10, 20, 30] """ @spec curried_peel(Extend.t(), fun()) :: Extend.t() def curried_peel(colink, data), do: curried_extend(data, colink) @doc """ ## Examples iex> composed = ...> fn xs -> List.first(xs) * 10 end ...> |> compose_colink(fn ys -> List.first(ys) - 10 end) ...> ...> extend([1, 2, 3], composed) [-90, -80, -70] iex> fn xs -> List.first(xs) * 10 end ...> |> compose_colink(fn ys -> List.first(ys) - 10 end) ...> |> compose_colink(fn zs -> List.first(zs) * 50 end) ...> |> peel([1, 2, 3]) [400, 900, 1400] iex> fn xs -> List.first(xs) * 10 end ...> |> compose_colink(fn ys -> List.first(ys) - 10 end) ...> |> compose_colink(fn zs -> List.first(zs) * 50 end) ...> |> compose_colink(fn zs -> List.first(zs) + 12 end) ...> |> peel([1, 2, 3]) [6400, 6900, 7400] """ @spec compose_colink(Extend.colink(), Extend.colink()) :: (Extend.t() -> any()) def compose_colink(g, f), do: fn x -> x |> curried_extend(f) |> g.() end @doc """ `pipe_colink/2` with functions curried. ## Examples iex> fn xs -> List.first(xs) * 10 end ...> |> pipe_compose_colink(fn ys -> List.first(ys) - 2 end) ...> |> peel([1, 2, 3]) [8, 18, 28] iex> composed = ...> fn xs -> List.first(xs) * 10 end ...> |> pipe_compose_colink(fn ys -> List.first(ys) - 2 end) ...> |> pipe_compose_colink(fn zs -> List.first(zs) * 5 end) ...> ...> extend([1, 2, 3], composed) [40, 90, 140] iex> fn xs -> List.first(xs) * 10 end ...> |> pipe_compose_colink(fn ys -> List.first(ys) - 2 end) ...> |> pipe_compose_colink(fn zs -> List.first(zs) * 5 end) ...> |> pipe_compose_colink(fn zs -> List.first(zs) + 1 end) ...> |> peel([1, 2, 3]) [41, 91, 141] """ @spec pipe_compose_colink(Extend.colink(), Extend.colink()) :: (Extend.t() -> any()) def pipe_compose_colink(f, g), do: compose_colink(g, f) end definst Witchcraft.Extend, for: Function do def nest(fun) do use Quark fn left -> fn right -> left |> Witchcraft.Semigroup.append(right) |> curry(fun).() end end end end definst Witchcraft.Extend, for: List do def nest([]), do: [] # Could be improved def nest(entire = [_head | tail]), do: [entire | nest(tail)] end definst Witchcraft.Extend, for: Tuple do custom_generator(_) do import TypeClass.Property.Generator, only: [generate: 1] {generate(nil), generate(nil)} end def nest({x, y}), do: {x, {x, y}} end