Witchcraft.Apply (Witchcraft v1.0.4) View Source
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.
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
inAlgae.Maybe
)
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]
Link to this section Summary
Functions
Operator alias for ap/2
Operator alias for reverse_ap/2
, moving in the pipe direction
Reverse arguments and sequencing of convey/2
.
Async version of ap/2
Async version of convey/2
Extends Functor.async_lift/2
to apply arguments to a binary function
Extends async_lift
to apply arguments to a ternary function
Extends async_lift
to apply arguments to a quaternary function
Extends async_over
to apply arguments to a binary function
Extends async_over
to apply arguments to a ternary function
Extends async_over
to apply arguments to a ternary function
Pipe arguments to functions, when both are wrapped in the same type of data structure.
Sequence actions, replacing the last argument with the first argument's values
Alias for convey/2
.
Extends Functor.lift/2
to apply arguments to a binary function
Extends lift
to apply arguments to a ternary function
Extends lift
to apply arguments to a quaternary function
Extends over
to apply arguments to a binary function
Extends over
to apply arguments to a ternary function
Extends over
to apply arguments to a ternary function
Same as ap/2
, but with all functions curried.
Same as convey/2
, but with all functions curried.
Sequence actions, replacing the first/previous values with the last argument
Link to this section Types
Link to this section Functions
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]
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]
Specs
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
Specs
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
Specs
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
Specs
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]
Specs
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]
Specs
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]
Specs
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]
Specs
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]
Specs
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]
Specs
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]
Specs
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}
Specs
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.
Examples
iex> [1, 2, 3]
...> |> hose([fn x -> x + 1 end, fn y -> y * 10 end])
[2, 10, 3, 20, 4, 30]
Specs
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]
Specs
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]
Specs
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]
Specs
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]
Specs
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]
Specs
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]
Specs
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]
Specs
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]
Specs
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}