View Source Witchcraft.Apply (Witchcraft v1.0.6-doma)
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
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
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
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
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
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
Operator alias for reverse_ap/2
, moving in the pipe direction
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
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]
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
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
Async version of ap/2
examples
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
Async version of convey/2
examples
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
Extends Functor.async_lift/2
to apply arguments to a binary function
examples
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]
Extends async_lift
to apply arguments to a ternary function
examples
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]
Extends async_lift
to apply arguments to a quaternary function
examples
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]
Extends async_over
to apply arguments to a binary function
examples
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]
Extends async_over
to apply arguments to a ternary function
examples
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]
Extends async_over
to apply arguments to a ternary function
examples
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]
Pipe arguments to functions, when both are wrapped in the same type of data structure.
examples
Examples
iex> [1, 2, 3]
...> |> convey([fn x -> x + 1 end, fn y -> y * 10 end])
[2, 10, 3, 20, 4, 30]
Sequence actions, replacing the last argument with the first argument's values
This is essentially a sequence of actions forgetting the second argument
examples
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}
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
Examples
iex> [1, 2, 3]
...> |> hose([fn x -> x + 1 end, fn y -> y * 10 end])
[2, 10, 3, 20, 4, 30]
Extends Functor.lift/2
to apply arguments to a binary function
examples
Examples
iex> lift([1, 2], [3, 4], &+/2)
[4, 5, 5, 6]
iex> [1, 2]
...> |> lift([3, 4], &*/2)
[3, 6, 4, 8]
Extends lift
to apply arguments to a ternary function
examples
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]
Extends lift
to apply arguments to a quaternary function
examples
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]
Extends over
to apply arguments to a binary function
examples
Examples
iex> over(&+/2, [1, 2], [3, 4])
[4, 5, 5, 6]
iex> (&*/2)
...> |> over([1, 2], [3, 4])
[3, 4, 6, 8]
Extends over
to apply arguments to a ternary function
examples
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]
Extends over
to apply arguments to a ternary function
examples
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]
Same as ap/2
, but with all functions curried.
examples
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]
Same as convey/2
, but with all functions curried.
examples
Examples
iex> [1, 2, 3]
...> |> supply([fn x -> x + 1 end, fn y -> y * 10 end])
[2, 10, 3, 20, 4, 30]
Sequence actions, replacing the first/previous values with the last argument
This is essentially a sequence of actions forgetting the first argument
examples
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}
Operator alias for reverse_ap/2
, moving in the pipe direction
examples
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]