-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #14 from robot-overlord/applicative
Applicative Functor + cleanup
- Loading branch information
Showing
25 changed files
with
717 additions
and
469 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
defprotocol Witchcraft.Applicative do | ||
@moduledoc """ | ||
Applicative functors provide a method of applying a function contained in a | ||
data structure to a value of the same type. This allows you to apply and compose | ||
functions to values while avoiding repeated manual wrapping and unwrapping | ||
of those values. | ||
# Properties | ||
## Identity | ||
`apply`ing a lifted `id` to some lifted value `v` does not change `v` | ||
`apply(v, wrap(&id(&1))) == v` | ||
## Composition | ||
`apply` composes normally. | ||
`apply((wrap &compose(&1,&2)), (apply(u,(apply(v, w))))) == apply(u,(apply(v, w)))` | ||
## Homomorphism | ||
`apply`ing a `wrap`ped function to a `wrap`ped value is the same as wrapping the | ||
result of the function on that value. | ||
`apply(wrap x, wrap f) == wrap f(x))` | ||
## Interchange | ||
The order does not matter when `apply`ing to a `wrap`ped value | ||
and a `wrap`ped function. | ||
`apply(wrap y, u) == apply(u, wrap &(lift(y, &1))` | ||
## Functor | ||
Being an applicative _functor_, `apply` behaves as `lift` on `wrap`ped values | ||
`lift(x, f) == apply(x, (wrap f))` | ||
# Notes | ||
Given that Elixir functons are right-associative, you can write clean looking, | ||
but much more ambiguous versions: | ||
`wrap(y) |> apply(u) == apply(u, wrap(&lift(y, &1)))` | ||
`lift(x, f) == apply(x, wrap f)` | ||
However, it is strongly recommended to include the parentheses for clarity. | ||
""" | ||
|
||
@fallback_to_any true | ||
|
||
@doc ~S""" | ||
Lift a pure value into a type provided by some specemin (usually the zeroth | ||
or empty value of that type, but not nessesarily). | ||
""" | ||
@spec wrap(any, any) :: any | ||
def wrap(specimen, bare) | ||
|
||
@doc ~S""" | ||
Sequentially apply lifted function(s) to lifted data. | ||
""" | ||
@spec apply(any, (... -> any)) :: any | ||
def apply(wrapped_value, wrapped_function) | ||
end | ||
|
||
defimpl Witchcraft.Applicative, for: Any do | ||
@doc ~S""" | ||
By default, use the true identity functor (ie: don't wrap) | ||
""" | ||
def wrap(_, bare_value), do: bare_value | ||
|
||
@doc ~S""" | ||
For un`wrap`ped values, treat `apply` as plain function application. | ||
""" | ||
def apply(bare_value, bare_function), do: Quark.Curry.curry(bare_function).(bare_value) | ||
end | ||
|
||
defimpl Witchcraft.Applicative, for: List do | ||
import Quark.Curry, only: [curry: 1] | ||
|
||
@doc ~S""" | ||
```elixir | ||
iex> wrap([], 0) | ||
[0] | ||
``` | ||
""" | ||
def wrap(_, bare), do: [bare] | ||
|
||
@doc ~S""" | ||
```elixir | ||
iex> import Kernel, except: [apply: 2] | ||
iex> apply([1,2,3], [&(&1 + 1), &(&1 * 10)]) | ||
[2,3,4,10,20,30] | ||
iex> import Kernel, except: [apply: 2] | ||
iex> import Witchcraft.Functor, only: [lift: 2] | ||
iex> apply([9,10,11], lift([1,2,3], &(fn x -> x * &1 end))) | ||
[9,10,11,18,20,22,27,30,33] | ||
``` | ||
""" | ||
def apply(_, []), do: [] | ||
def apply(values, [fun|funs]) do | ||
Enum.map(values, curry(fun)) ++ Witchcraft.Applicative.apply(values, funs) | ||
end | ||
end | ||
|
||
defimpl Witchcraft.Applicative, for: Witchcraft.Id do | ||
import Quark.Curry, only: [curry: 1] | ||
alias Witchcraft.Id, as: Id | ||
|
||
@doc ~S""" | ||
```elixir | ||
iex> %Witchcraft.Id{} |> wrap(9) | ||
%Witchcraft.Id{id: 9} | ||
``` | ||
""" | ||
def wrap(_, bare), do: %Witchcraft.Id{id: bare} | ||
|
||
@doc ~S""" | ||
```elixir | ||
iex> import Kernel, except: [apply: 2] | ||
iex> apply(%Witchcraft.Id{id: 42}, %Witchcraft.Id{id: &(&1 + 1)}) | ||
%Witchcraft.Id{id: 43} | ||
iex> import Kernel, except: [apply: 2] | ||
iex> import Witchcraft.Functor, only: [lift: 2] | ||
iex> alias Witchcraft.Id, as: Id | ||
iex> apply(%Id{id: 9}, lift(%Id{id: 2}, &(fn x -> x + &1 end))) | ||
%Witchcraft.Id{id: 11} | ||
``` | ||
""" | ||
def apply(%Id{id: value}, %Id{id: fun}), do: %Id{id: curry(fun).(value)} | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
defmodule Witchcraft.Applicative.Function do | ||
@moduledoc ~S""" | ||
Function helpers, derivatives and operators for `Witchcraft.Applicative` | ||
""" | ||
|
||
import Kernel, except: [apply: 2] | ||
|
||
import Quark, only: [id: 1, flip: 1, constant: 2] | ||
import Quark.Curry, only: [curry: 1] | ||
|
||
import Witchcraft.Applicative, only: [apply: 2] | ||
import Witchcraft.Functor.Operator, only: [<~: 2, ~>: 2] | ||
|
||
@doc ~S""" | ||
`lift` a function that takes a list of arguments | ||
```elixir | ||
iex> lift([[1,2,3], [4,5,6]], &(&1 + &2)) | ||
[5,6,7,6,7,8,7,8,9] | ||
iex> lift([[1,2], [3,4], [5,6]], &(&1 + &2 + &3)) | ||
[9,10,10,11,10,11,11,12] | ||
iex> lift([[1,2], [3,4], [5,6], [7,8]], &(&1 + &2 + &3 + &4)) | ||
[16,17,17,18,17,18,18,19,17,18,18,19,18,19,19,20] | ||
``` | ||
""" | ||
@spec lift(any, (... -> any)) :: any | ||
def lift([value], fun), do: value ~> curry(fun) | ||
def lift([head|tail], fun), do: Enum.reduce(tail, lift([head], fun), &apply/2) | ||
|
||
@doc ~S""" | ||
Sequentially `apply`, and discard the second value of each pair. | ||
""" | ||
@spec seq_first([any]) :: any | ||
def seq_first([a,b]), do: lift([a,b], &constant/2) | ||
|
||
@doc ~S""" | ||
Sequentially `apply`, and discard the first value of each pair. | ||
""" | ||
@spec seq_second([any]) :: any | ||
def seq_second([a,b]), do: lift([a,b], fn x -> constant(x, &id/1) end) | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
defmodule Witchcraft.Applicative.Operator do | ||
@moduledoc ~S""" | ||
""" | ||
|
||
import Kernel, except: [apply: 2] | ||
import Witchcraft.Applicative, only: [apply: 2] | ||
import Witchcraft.Applicative.Function, only: [lift: 2] | ||
|
||
@doc ~S""" | ||
Infix alias for `Witchcraft.Applicative.apply`. If chaining, be sure to wrap | ||
each layer in parentheses, as `~>>` and `~>` are left associative. | ||
```elixir | ||
iex> [1,2,3] ~>> [&(&1 + 1), &(&1 * 10)] | ||
[2,3,4,10,20,30] | ||
# iex> [9, 10] ~>> (Witchcraft.Applicative.Function.lift [1,2,3], &(fn x -> x + &1 end)) | ||
iex> [9, 10] ~>> ([1,2,3] ~> &(fn x -> x * &1 end)) | ||
[9, 10, 18, 20, 27, 30] | ||
``` | ||
""" | ||
@spec any ~>> any :: any | ||
def value ~>> func, do: apply(value, func) | ||
|
||
@doc ~S""" | ||
Infix alias for `Witchcraft.Applicative.apply`, with arguments reversed. | ||
This version is preferred, as it makes chaining arguments along wrapped | ||
partial applications clearer when reading left-to-right. | ||
```elixir | ||
iex> [&(&1 + 1), &(&1 * 10)] <<~ [1,2,3] | ||
[2,3,4,10,20,30] | ||
iex> (&(fn x -> x * &1 end)) <~ [1,2,3] <<~ [9,10,11] | ||
[9,10,11,18,20,22,27,30,33] | ||
``` | ||
""" | ||
@spec any <<~ any :: any | ||
def func <<~ value, do: value ~>> func | ||
|
||
defdelegate functor_value ~> bare_function, to: Witchcraft.Functor, as: :lift | ||
def bare_function <~ functor_value, do: functor_value ~> bare_function | ||
end |
Oops, something went wrong.