Packages

FunLand adds Behaviours to define Algebraic Data Types ('Container' data types) to Elixir, such as Functors, Monoids and Monads.

Current section

Files

Jump to
fun_land lib fun_land appliable.ex
Raw

lib/fun_land/appliable.ex

defmodule FunLand.Appliable do
@doc """
Something is Appliable if you can _apply_ one of it (containing one or multiple functions) _with_ another.
Appliable is mostly born out of the needs to apply a function that is already wrapped in a Mappable:
- If you had a bare function, you could use `Mappable.map/2` to apply it over a Mappable.
- If however, you have a function already inside a Mappable, a new operation has to be defined to apply it over a Mappable (of the same kind).
This operation is called `apply_with/2`.
'a function inside a Mappable' is something that happens when you partially apply functions, which isn't that common in Elixir because functions are not automatically curried.
### Currying and Partial Application
As `apply_with` works only applies a single argument per function at a time, it works the best when used with curried functions.
In Elixir, functions are no curried by default.
Fortunately, there exists the [Currying](https://hex.pm/packages/currying) library, which transforms your normal functions into curried functions.
If you want to be able to use Applicative to its fullest potential, instead of calling `fun.(b)` in your implementation, use `Currying.curry(fun).(b)`
_________
To be Appliable something also has to be Mappable.
To make your data structure Appliable, use `use Appliable` in its module, and implement both Appliable's `apply_with/2` and Mappable's `map/2`.
## Fruit Salad Example
Say we have a bowl with a partiall-made fruit-salad.
We have a second bowl, which contains some (peeled) bananas.
We would like to add these bananas to the fruit salad.
This would be easy if we had our partially-made fruit-salad, as we could just _map_ the 'combine a banana with some fruit salad' operation over the bowl of bananas.
However, we don't 'just' have the partially-made fruit-salad, as this would make a big mess of our kitchen countertop.
In fact, it is very likely that this bowl-with partially-made fruit salad was the result of combining (`mapping`) earlier ingredients in bowls.
So, we need something similar to `map`, but instead of taking 'just' an operation, we use a bowl with that operation.
For the fruit salad bowl, we could define it as 'take some fruit-salad from Bowl A, combine it with a banana in Bowl B. -> repeat until bananas and fruit-salad are fully combined'.
This is called `apply_with`.
Note that, because the part that changes more often is the Appliable with the (partially-applied) function (in other words: The bowl with the partially-made fruit salad),
the parameters of this functions are the reverse of `Mappable.map`.
## In Other Environments
- In Haskell, `Appliable.apply_with` is known by the uninformative name `ap`, often written as `<$>`.
- In Category Theory, something that is Appliable is called an *Apply*.
"""
@type appliable(_) :: FunLand.adt
@callback apply_with(appliable((b -> c)), appliable(b)) :: appliable(c) when b: any, c: any
defmacro __using__(_opts) do
quote do
use FunLand.Mappable
@behaviour FunLand.Appliable
end
end
defdelegate map(a, fun), to: FunLand.Mappable
@doc """
Applies `appliable_with_function_inside`, which should only contain functions, with as arguments
the elements inside `appliable_b`.
For a List, this means that the list(s) that are made by mapping each of the functions inside
`appliable_with_function_inside` over the elements of `appliable_b` are concatenated, so a single list of all
results is returned.
iex> [&(&1 + 1), &(&1 - 1)] |> FunLand.Appliable.apply_with([1, 2])
[2, 3, 0, 1]
iex> [Currying.curry(&+/2), Currying.curry(&-/2)] |> FunLand.Appliable.apply_with([10, 20]) |> FunLand.Appliable.apply_with([3,4])
[13, 14, 23, 24, 7, 6, 17, 16]
For `Maybe`, whenever one of the two arguments is `Nothing`, `Nothing` will be returned. If both are filled, then a result will be computed,
and this result will be returned, wrapped in a new Maybe.
"""
def apply_with(appliable_with_function_inside, appliable_b)
def apply_with(a = %appliable_module{}, b = %appliable_module{}) do
appliable_module.apply_with(a, b)
end
use FunLand.Helper.GuardMacros
for {guard, module} <- FunLand.Builtin.__builtin__ do
def apply_with(a, b) when unquote(guard)(a) and unquote(guard)(b) do
apply(unquote(module), :apply_with, [a, b])
end
end
end