import TypeClass
defclass Witchcraft.Apply do
@moduledoc """
An extension of `Witchcraft.Functor`, `Apply` provides a way to _apply_ arguments
to functions when both are wrapped in the same kind of container. This can be
seen as running function application "in a context".
For a nice, illustrated introduction,
see [Functors, Applicatives, And Monads In Pictures](http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html).
## Graphically
If function application looks like this
data |> function == result
and a functor looks like this
%Container<data> ~> function == %Container<result>
then an apply looks like
%Container<data> ~>> %Container<function> == %Container<result>
which is similar to function application inside containers, plus the ability to
attach special effects to applications.
data --------------- function ---------------> result
%Container<data> --- %Container<function> ---> %Container<result>
This lets us do functorial things like
* continue applying values to a curried function resulting from a `Witchcraft.Functor.lift/2`
* apply multiple functions to multiple arguments (with lists)
* propogate some state (like [`Nothing`](https://hexdocs.pm/algae/Algae.Maybe.Nothing.html#content)
in [`Algae.Maybe`](https://hexdocs.pm/algae/Algae.Maybe.html#content))
but now with a much larger number of arguments, reuse partially applied functions,
and run effects with the function container as well as the data container.
## Examples
iex> ap([fn x -> x + 1 end, fn y -> y * 10 end], [1, 2, 3])
[2, 3, 4, 10, 20, 30]
iex> [100, 200]
...> |> Witchcraft.Functor.lift(fn(x, y, z) -> x * y / z end)
...> |> provide([5, 2])
...> |> provide([100, 50])
[5.0, 10.0, 2.0, 4.0, 10.0, 20.0, 4.0, 8.0]
# ↓ ↓
# 100 * 5 / 100 200 * 5 / 50
iex> import Witchcraft.Functor
...>
...> [100, 200]
...> ~> fn(x, y, z) ->
...> x * y / z
...> end <<~ [5, 2]
...> <<~ [100, 50]
[5.0, 10.0, 2.0, 4.0, 10.0, 20.0, 4.0, 8.0]
# ↓ ↓
# 100 * 5 / 100 200 * 5 / 50
%Algae.Maybe.Just{just: 42}
~> fn(x, y, z) ->
x * y / z
end <<~ %Algae.Maybe.Nothing{}
<<~ %Algae.Maybe.Just{just: 99}
#=> %Algae.Maybe.Nothing{}
## `convey` vs `ap`
`convey` and `ap` essentially associate in opposite directions. For example,
large data is _usually_ more efficient with `ap`, and large numbers of
functions are _usually_ more efficient with `convey`.
It's also more consistent consistency. In Elixir, we like to think of a "subject"
being piped through a series of transformations. This places the function argument
as the second argument. In `Witchcraft.Functor`, this was of little consequence.
However, in `Apply`, we're essentially running superpowered function application.
`ap` is short for `apply`, as to not conflict with `Kernel.apply/2`, and is meant
to respect a similar API, with the function as the first argument. This also reads
nicely when piped, as it becomes `[funs] |> ap([args1]) |> ap([args2])`,
which is similar in structure to `fun.(arg2).(arg1)`.
With potentially multiple functions being applied over potentially
many arguments, we need to worry about ordering. `convey` not only flips
the order of arguments, but also who is in control of ordering.
`convey` typically runs each function over all arguments (`first_fun ⬸ all_args`),
and `ap` runs all functions for each element (`first_arg ⬸ all_funs`).
This may change the order of results, and is a feature, not a bug.
iex> [1, 2, 3]
...> |> convey([&(&1 + 1), &(&1 * 10)])
[
2, 10, # [(1 + 1), (1 * 10)]
3, 20, # [(2 + 1), (2 * 10)]
4, 30 # [(3 + 1), (3 * 10)]
]
iex> [&(&1 + 1), &(&1 * 10)]
...> |> ap([1, 2, 3])
[
2, 3, 4, # [(1 + 1), (2 + 1), (3 + 1)]
10, 20, 30 # [(1 * 10), (2 * 10), (3 * 10)]
]
## Type Class
An instance of `Witchcraft.Apply` must also implement `Witchcraft.Functor`,
and define `Witchcraft.Apply.convey/2`.
Functor [map/2]
↓
Apply [convey/2]
"""
alias __MODULE__
alias Witchcraft.Functor
extend Witchcraft.Functor
use Witchcraft.Internal, deps: [Witchcraft.Functor]
use Witchcraft.Functor
use Quark
@type t :: any()
@type fun :: any()
where do
@doc """
Pipe arguments to functions, when both are wrapped in the same
type of data structure.
## Examples
iex> [1, 2, 3]
...> |> convey([fn x -> x + 1 end, fn y -> y * 10 end])
[2, 10, 3, 20, 4, 30]
"""
@spec convey(Apply.t(), Apply.fun()) :: Apply.t()
def convey(wrapped_args, wrapped_funs)
end
properties do
def composition(data) do
alias Witchcraft.Functor
use Quark
as = data |> generate() |> Functor.map(&inspect/1)
fs = data |> generate() |> Functor.replace(fn x -> x <> x end)
gs = data |> generate() |> Functor.replace(fn y -> y <> "foo" end)
left = Apply.convey(Apply.convey(as, gs), fs)
right =
fs
|> Functor.lift(&compose/2)
|> (fn x -> Apply.convey(gs, x) end).()
|> (fn y -> Apply.convey(as, y) end).()
equal?(left, right)
end
end
@doc """
Alias for `convey/2`.
Why "hose"?
* Pipes (`|>`) are application with arguments flipped
* `ap/2` is like function application "in a context"
* The opposite of `ap` is a contextual pipe
* `hose`s are a kind of flexible pipe
Q.E.D.
![](http://s2.quickmeme.com/img/fd/fd0baf5ada879021c32129fc7dea679bd7666e708df8ca8ca536da601ea3d29e.jpg)
## Examples
iex> [1, 2, 3]
...> |> hose([fn x -> x + 1 end, fn y -> y * 10 end])
[2, 10, 3, 20, 4, 30]
"""
@spec hose(Apply.t(), Apply.fun()) :: Apply.t()
def hose(wrapped_args, wrapped_funs), do: convey(wrapped_args, wrapped_funs)
@doc """
Reverse arguments and sequencing of `convey/2`.
Conceptually this makes operations happen in
a different order than `convey/2`, with the left-side arguments (functions) being
run on all right-side arguments, in that order. We're altering the _sequencing_
of function applications.
## Examples
iex> ap([fn x -> x + 1 end, fn y -> y * 10 end], [1, 2, 3])
[2, 3, 4, 10, 20, 30]
# For comparison
iex> convey([1, 2, 3], [fn x -> x + 1 end, fn y -> y * 10 end])
[2, 10, 3, 20, 4, 30]
iex> [100, 200]
...> |> Witchcraft.Functor.lift(fn(x, y, z) -> x * y / z end)
...> |> ap([5, 2])
...> |> ap([100, 50])
[5.0, 10.0, 2.0, 4.0, 10.0, 20.0, 4.0, 8.0]
# ↓ ↓
# 100 * 5 / 100 200 * 5 / 50
"""
@spec ap(Apply.fun(), Apply.t()) :: Apply.t()
def ap(wrapped_funs, wrapped) do
lift(wrapped, wrapped_funs, fn arg, fun -> fun.(arg) end)
end
@doc """
Async version of `convey/2`
## Examples
iex> [1, 2, 3]
...> |> async_convey([fn x -> x + 1 end, fn y -> y * 10 end])
[2, 10, 3, 20, 4, 30]
0..10_000
|> Enum.to_list()
|> async_convey([
fn x ->
Process.sleep(500)
x + 1
end,
fn y ->
Process.sleep(500)
y * 10
end
])
#=> [1, 0, 2, 10, 3, 30, ...] in around a second
"""
@spec async_convey(Apply.t(), Apply.fun()) :: Apply.t()
def async_convey(wrapped_args, wrapped_funs) do
wrapped_args
|> convey(
lift(wrapped_funs, fn fun, arg ->
Task.async(fn ->
fun.(arg)
end)
end)
)
|> map(&Task.await/1)
end
@doc """
Async version of `ap/2`
## Examples
iex> [fn x -> x + 1 end, fn y -> y * 10 end]
...> |> async_ap([1, 2, 3])
[2, 3, 4, 10, 20, 30]
[
fn x ->
Process.sleep(500)
x + 1
end,
fn y ->
Process.sleep(500)
y * 10
end
]
|> async_ap(Enum.to_list(0..10_000))
#=> [1, 2, 3, 4, ...] in around a second
"""
@spec async_ap(Apply.fun(), Apply.t()) :: Apply.t()
def async_ap(wrapped_funs, wrapped_args) do
wrapped_funs
|> lift(fn fun, arg ->
Task.async(fn ->
fun.(arg)
end)
end)
|> ap(wrapped_args)
|> map(&Task.await/1)
end
@doc """
Operator alias for `ap/2`
Moves against the pipe direction, but in the order of normal function application
## Examples
iex> [fn x -> x + 1 end, fn y -> y * 10 end] <<~ [1, 2, 3]
[2, 3, 4, 10, 20, 30]
iex> import Witchcraft.Functor
...>
...> [100, 200]
...> ~> fn(x, y, z) -> x * y / z
...> end <<~ [5, 2]
...> <<~ [100, 50]
...> ~> fn x -> x + 1 end
[6.0, 11.0, 3.0, 5.0, 11.0, 21.0, 5.0, 9.0]
iex> import Witchcraft.Functor, only: [<~: 2]
...> fn(a, b, c, d) -> a * b - c + d end <~ [1, 2] <<~ [3, 4] <<~ [5, 6] <<~ [7, 8]
[5, 6, 4, 5, 6, 7, 5, 6, 8, 9, 7, 8, 10, 11, 9, 10]
"""
defalias wrapped_funs <<~ wrapped, as: :provide
@doc """
Operator alias for `reverse_ap/2`, moving in the pipe direction
## Examples
iex> [1, 2, 3] ~>> [fn x -> x + 1 end, fn y -> y * 10 end]
[2, 10, 3, 20, 4, 30]
iex> import Witchcraft.Functor
...>
...> [100, 50]
...> ~>> ([5, 2] # Note the bracket
...> ~>> ([100, 200] # on both `Apply` lines
...> ~> fn(x, y, z) -> x * y / z end))
[5.0, 10.0, 2.0, 4.0, 10.0, 20.0, 4.0, 8.0]
"""
defalias wrapped ~>> wrapped_funs, as: :supply
@doc """
Same as `convey/2`, but with all functions curried.
## Examples
iex> [1, 2, 3]
...> |> supply([fn x -> x + 1 end, fn y -> y * 10 end])
[2, 10, 3, 20, 4, 30]
"""
@spec supply(Apply.t(), Apply.fun()) :: Apply.t()
def supply(args, funs), do: convey(args, Functor.map(funs, &curry/1))
@doc """
Same as `ap/2`, but with all functions curried.
## Examples
iex> [&+/2, &*/2]
...> |> provide([1, 2, 3])
...> |> ap([4, 5, 6])
[5, 6, 7, 6, 7, 8, 7, 8, 9, 4, 5, 6, 8, 10, 12, 12, 15, 18]
"""
@spec provide(Apply.fun(), Apply.t()) :: Apply.t()
def provide(funs, args), do: funs |> Functor.map(&curry/1) |> ap(args)
@doc """
Sequence actions, replacing the first/previous values with the last argument
This is essentially a sequence of actions forgetting the first argument
## Examples
iex> [1, 2, 3]
...> |> Witchcraft.Apply.then([4, 5, 6])
...> |> Witchcraft.Apply.then([7, 8, 9])
[
7, 8, 9,
7, 8, 9,
7, 8, 9,
7, 8, 9,
7, 8, 9,
7, 8, 9,
7, 8, 9,
7, 8, 9,
7, 8, 9
]
iex> {1, 2, 3} |> Witchcraft.Apply.then({4, 5, 6}) |> Witchcraft.Apply.then({7, 8, 9})
{12, 15, 9}
"""
@spec then(Apply.t(), Apply.t()) :: Apply.t()
def then(wrapped_a, wrapped_b), do: over(&Quark.constant(&2, &1), wrapped_a, wrapped_b)
@doc """
Sequence actions, replacing the last argument with the first argument's values
This is essentially a sequence of actions forgetting the second argument
## Examples
iex> [1, 2, 3]
...> |> following([3, 4, 5])
...> |> following([5, 6, 7])
[
1, 1, 1, 1, 1, 1, 1, 1, 1,
2, 2, 2, 2, 2, 2, 2, 2, 2,
3, 3, 3, 3, 3, 3, 3, 3, 3
]
iex> {1, 2, 3} |> following({4, 5, 6}) |> following({7, 8, 9})
{12, 15, 3}
"""
@spec following(Apply.t(), Apply.t()) :: Apply.t()
def following(wrapped_a, wrapped_b), do: lift(wrapped_b, wrapped_a, &Quark.constant(&2, &1))
@doc """
Extends `Functor.lift/2` to apply arguments to a binary function
## Examples
iex> lift([1, 2], [3, 4], &+/2)
[4, 5, 5, 6]
iex> [1, 2]
...> |> lift([3, 4], &*/2)
[3, 6, 4, 8]
"""
@spec lift(Apply.t(), Apply.t(), fun()) :: Apply.t()
def lift(a, b, fun) do
a
|> lift(fun)
|> (fn f -> convey(b, f) end).()
end
@doc """
Extends `lift` to apply arguments to a ternary function
## Examples
iex> lift([1, 2], [3, 4], [5, 6], fn(a, b, c) -> a * b - c end)
[-2, -3, 1, 0, -1, -2, 3, 2]
"""
@spec lift(Apply.t(), Apply.t(), Apply.t(), fun()) :: Apply.t()
def lift(a, b, c, fun), do: a |> lift(b, fun) |> ap(c)
@doc """
Extends `lift` to apply arguments to a quaternary function
## Examples
iex> lift([1, 2], [3, 4], [5, 6], [7, 8], fn(a, b, c, d) -> a * b - c + d end)
[5, 6, 4, 5, 8, 9, 7, 8, 6, 7, 5, 6, 10, 11, 9, 10]
"""
@spec lift(Apply.t(), Apply.t(), Apply.t(), Apply.t(), fun()) :: Apply.t()
def lift(a, b, c, d, fun), do: a |> lift(b, c, fun) |> ap(d)
@doc """
Extends `Functor.async_lift/2` to apply arguments to a binary function
## Examples
iex> async_lift([1, 2], [3, 4], &+/2)
[4, 5, 5, 6]
iex> [1, 2]
...> |> async_lift([3, 4], &*/2)
[3, 6, 4, 8]
"""
@spec async_lift(Apply.t(), Apply.t(), fun()) :: Apply.t()
def async_lift(a, b, fun) do
a
|> async_lift(fun)
|> (fn f -> async_convey(b, f) end).()
end
@doc """
Extends `async_lift` to apply arguments to a ternary function
## Examples
iex> async_lift([1, 2], [3, 4], [5, 6], fn(a, b, c) -> a * b - c end)
[-2, -3, 1, 0, -1, -2, 3, 2]
"""
@spec async_lift(Apply.t(), Apply.t(), Apply.t(), fun()) :: Apply.t()
def async_lift(a, b, c, fun), do: a |> async_lift(b, fun) |> async_ap(c)
@doc """
Extends `async_lift` to apply arguments to a quaternary function
## Examples
iex> async_lift([1, 2], [3, 4], [5, 6], [7, 8], fn(a, b, c, d) -> a * b - c + d end)
[5, 6, 4, 5, 8, 9, 7, 8, 6, 7, 5, 6, 10, 11, 9, 10]
"""
@spec async_lift(Apply.t(), Apply.t(), Apply.t(), Apply.t(), fun()) :: Apply.t()
def async_lift(a, b, c, d, fun), do: a |> async_lift(b, c, fun) |> async_ap(d)
@doc """
Extends `over` to apply arguments to a binary function
## Examples
iex> over(&+/2, [1, 2], [3, 4])
[4, 5, 5, 6]
iex> (&*/2)
...> |> over([1, 2], [3, 4])
[3, 4, 6, 8]
"""
@spec over(fun(), Apply.t(), Apply.t()) :: Apply.t()
def over(fun, a, b), do: a |> lift(fun) |> ap(b)
@doc """
Extends `over` to apply arguments to a ternary function
## Examples
iex> fn(a, b, c) -> a * b - c end
iex> |> over([1, 2], [3, 4], [5, 6])
[-2, -3, -1, -2, 1, 0, 3, 2]
"""
@spec over(fun(), Apply.t(), Apply.t(), Apply.t()) :: Apply.t()
def over(fun, a, b, c), do: fun |> over(a, b) |> ap(c)
@doc """
Extends `over` to apply arguments to a ternary function
## Examples
iex> fn(a, b, c) -> a * b - c end
...> |> over([1, 2], [3, 4], [5, 6])
[-2, -3, -1, -2, 1, 0, 3, 2]
"""
@spec over(fun(), Apply.t(), Apply.t(), Apply.t(), Apply.t()) :: Apply.t()
def over(fun, a, b, c, d), do: fun |> over(a, b, c) |> ap(d)
@doc """
Extends `async_over` to apply arguments to a binary function
## Examples
iex> async_over(&+/2, [1, 2], [3, 4])
[4, 5, 5, 6]
iex> (&*/2)
...> |> async_over([1, 2], [3, 4])
[3, 4, 6, 8]
"""
@spec async_over(fun(), Apply.t(), Apply.t()) :: Apply.t()
def async_over(fun, a, b), do: a |> lift(fun) |> async_ap(b)
@doc """
Extends `async_over` to apply arguments to a ternary function
## Examples
iex> fn(a, b, c) -> a * b - c end
iex> |> async_over([1, 2], [3, 4], [5, 6])
[-2, -3, -1, -2, 1, 0, 3, 2]
"""
@spec async_over(fun(), Apply.t(), Apply.t(), Apply.t()) :: Apply.t()
def async_over(fun, a, b, c), do: fun |> async_over(a, b) |> async_ap(c)
@doc """
Extends `async_over` to apply arguments to a ternary function
## Examples
iex> fn(a, b, c) -> a * b - c end
...> |> async_over([1, 2], [3, 4], [5, 6])
[-2, -3, -1, -2, 1, 0, 3, 2]
"""
@spec async_over(fun(), Apply.t(), Apply.t(), Apply.t(), Apply.t()) :: Apply.t()
def async_over(fun, a, b, c, d), do: fun |> async_over(a, b, c) |> async_ap(d)
end
definst Witchcraft.Apply, for: Function do
use Quark
def convey(g, f), do: fn x -> curry(f).(x).(curry(g).(x)) end
end
definst Witchcraft.Apply, for: List do
def convey(val_list, fun_list) when is_list(fun_list) do
Enum.flat_map(val_list, fn val ->
Enum.map(fun_list, fn fun -> fun.(val) end)
end)
end
end
# Contents must be semigroups
definst Witchcraft.Apply, for: Tuple do
import TypeClass.Property.Generator, only: [generate: 1]
use Witchcraft.Semigroup
custom_generator(_) do
{generate(""), generate(1), generate(0), generate(""), generate(""), generate("")}
end
def convey({v, w}, {a, fun}), do: {v <> a, fun.(w)}
def convey({v, w, x}, {a, b, fun}), do: {v <> a, w <> b, fun.(x)}
def convey({v, w, x, y}, {a, b, c, fun}), do: {v <> a, w <> b, x <> c, fun.(y)}
def convey({v, w, x, y, z}, {a, b, c, d, fun}) do
{
a <> v,
b <> w,
c <> x,
d <> y,
fun.(z)
}
end
def convey(tuple_a, tuple_b) when tuple_size(tuple_a) == tuple_size(tuple_b) do
last_index = tuple_size(tuple_a) - 1
tuple_a
|> Tuple.to_list()
|> Enum.zip(Tuple.to_list(tuple_b))
|> Enum.with_index()
|> Enum.map(fn
{{arg, fun}, ^last_index} -> fun.(arg)
{{left, right}, _} -> left <> right
end)
|> List.to_tuple()
end
end