Skip to content

Commit

Permalink
Merge pull request #14 from robot-overlord/applicative
Browse files Browse the repository at this point in the history
Applicative Functor + cleanup
  • Loading branch information
expede committed Jan 12, 2016
2 parents 3857a61 + c6fa1ac commit 947e60d
Show file tree
Hide file tree
Showing 25 changed files with 717 additions and 469 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -10,6 +10,6 @@ Functors, monads, arrows, and categories

```
def deps do
[{:witchcraft, "~> 0.2.0"}]
[{:witchcraft, "~> 0.3.0"}]
end
```
25 changes: 0 additions & 25 deletions lib/witchcraft.ex

This file was deleted.

48 changes: 0 additions & 48 deletions lib/witchcraft/adt/maybe.ex

This file was deleted.

50 changes: 0 additions & 50 deletions lib/witchcraft/adt/maybe_plus.ex

This file was deleted.

146 changes: 146 additions & 0 deletions lib/witchcraft/applicative.ex
@@ -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
46 changes: 46 additions & 0 deletions lib/witchcraft/applicative/function.ex
@@ -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
50 changes: 50 additions & 0 deletions lib/witchcraft/applicative/operator.ex
@@ -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

0 comments on commit 947e60d

Please sign in to comment.