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

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.

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

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

Link to this function

wrapped_funs <<~ wrapped

View Source

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]
Link to this function

wrapped ~>> wrapped_funs

View Source

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]
Link to this function

ap(wrapped_funs, wrapped)

View Source

Specs

ap((... -> any()), t()) :: t()

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
Link to this function

async_ap(wrapped_funs, wrapped_args)

View Source

Specs

async_ap((... -> any()), t()) :: t()

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
Link to this function

async_convey(wrapped_args, wrapped_funs)

View Source

Specs

async_convey(t(), (... -> any())) :: t()

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

async_lift(t(), t(), (... -> any())) :: t()

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]
Link to this function

async_lift(a, b, c, fun)

View Source

Specs

async_lift(t(), t(), t(), (... -> any())) :: t()

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]
Link to this function

async_lift(a, b, c, d, fun)

View Source

Specs

async_lift(t(), t(), t(), t(), (... -> any())) :: t()

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

async_over((... -> any()), t(), t()) :: t()

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]
Link to this function

async_over(fun, a, b, c)

View Source

Specs

async_over((... -> any()), t(), t(), t()) :: t()

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]
Link to this function

async_over(fun, a, b, c, d)

View Source

Specs

async_over((... -> any()), t(), t(), t(), t()) :: t()

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]
Link to this function

convey(wrapped_args, wrapped_funs)

View Source

Specs

convey(t(), (... -> any())) :: t()

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]
Link to this function

following(wrapped_a, wrapped_b)

View Source

Specs

following(t(), t()) :: t()

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}
Link to this function

hose(wrapped_args, wrapped_funs)

View Source

Specs

hose(t(), (... -> any())) :: t()

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
  • hoses 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

lift(t(), t(), (... -> any())) :: t()

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

lift(t(), t(), t(), (... -> any())) :: t()

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

lift(t(), t(), t(), t(), (... -> any())) :: t()

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

over((... -> any()), t(), t()) :: t()

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

over((... -> any()), t(), t(), t()) :: t()

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

over((... -> any()), t(), t(), t(), t()) :: t()

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

provide((... -> any()), t()) :: t()

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

supply(t(), (... -> any())) :: t()

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]
Link to this function

then(wrapped_a, wrapped_b)

View Source

Specs

then(t(), t()) :: t()

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}