import TypeClass
defclass Witchcraft.Monad do
@moduledoc """
Very similar to `Chain`, `Monad` provides a way to link actions, and a way
to bring plain values into the correct context (`Applicative`).
This allows us to view actions in a full framework along the lines of
functor and applicative:
data ---------------- function ----------------------------> result
| | |
of(Container, data) of/2, or similar of(Container, result)
↓ ↓ ↓
%Container<data> --- (data -> %Container<updated_data>) ---> %Container<updated_data>
As you can see, the linking function may just be `of` now that we have that.
For a nice, illustrated introduction,
see [Functors, Applicatives, And Monads In Pictures](http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html).
Having `of` also lets us enhance do-notation with a convenient `return` function (see `monad/2`)
## Type Class
An instance of `Witchcraft.Monad` must also implement `Witchcraft.Applicative`
and `Wicthcraft.Chainable`.
Functor [map/2]
↓
Apply [convey/2]
↓ ↓
[of/2] Applicative Chain [chain/2]
↓ ↓
Monad
[_]
"""
alias Witchcraft.Chain
extend Witchcraft.Applicative
extend Witchcraft.Chain
use Witchcraft.Internal, deps: [Witchcraft.Applicative, Witchcraft.Chain]
use Witchcraft.Applicative
use Witchcraft.Chain
@type t :: any()
properties do
import Witchcraft.Applicative
import Witchcraft.Chain
def left_identity(data) do
a = generate(data)
f = &Witchcraft.Functor.replace(a, inspect(&1))
left = a |> of(a) |> chain(f)
right = f.(a)
equal?(left, right)
end
def right_identity(data) do
a = generate(data)
left = a >>> (&of(a, &1))
equal?(a, left)
end
end
@doc """
Asynchronous variant of `Witchcraft.Chain.chain/2`.
Note that _each_ `async_chain` call awaits that step's completion. This is a
feature not a bug, since `chain` can introduce dependencies between nested links.
However, this means that the async features on only really useful on larger data sets,
because otherwise we're just sparking tasks and immediaetly waiting a single application.
## Examples
iex> async_chain([1, 2, 3], fn x -> [x, x] end)
[1, 1, 2, 2, 3, 3]
iex> async_chain([1, 2, 3], fn x ->
...> async_chain([x + 1], fn y ->
...> [x * y]
...> end)
...> end)
[2, 6, 12]
0..10_000
|> Enum.to_list()
|> async_chain(fn x ->
async_chain([x + 1], fn y ->
Process.sleep(500)
[x * y]
end)
end)
#=> [0, 2, 6, 12, 20, 30, 42, ...] in around a second
"""
@spec async_chain(Chain.t(), Chain.link()) :: Chain.t()
def async_chain(chainable, link) do
chainable
|> chain(fn x ->
# credo:disable-for-lines:3 Credo.Check.Refactor.PipeChainStart
fn -> link.(x) end
|> Task.async()
|> to(chainable)
end)
|> chain(&Task.await/1)
end
@doc "Alias for `async_chain/2`"
@spec async_bind(Chain.t(), Chain.link()) :: Chain.t()
def async_bind(chainable, link), do: async_chain(chainable, link)
@doc """
Asynchronous variant of `Witchcraft.Chain.draw/2`.
Note that _each_ `async_draw` call awaits that step's completion. This is a
feature not a bug, since `chain` can introduce dependencies between nested links.
However, this means that the async features on only really useful on larger data sets,
because otherwise we're just sparking tasks and immediaetly waiting a single application.
## Examples
iex> async_draw(fn x -> [x, x] end, [1, 2, 3])
[1, 1, 2, 2, 3, 3]
iex> (fn y -> [y * 5, y * 10] end)
...> |> async_draw(fn x -> [x, x] end
...> |> async_draw([1, 2, 3])) # note the "extra" closing paren
[5, 10, 5, 10, 10, 20, 10, 20, 15, 30, 15, 30]
iex> fn x ->
...> fn y ->
...> [x * y]
...> end
...> |> async_draw([x + 1])
...> end
...> |> async_draw([1, 2, 3])
[2, 6, 12]
fn x ->
fn y ->
Process.sleep(500)
[x * y]
end
|> async_draw([x + 1])
end
|> async_draw(Enum.to_list(0..10_000))
[0, 2, 6, 12, ...] # in under a second
"""
@spec async_draw(Chain.t(), Chain.link()) :: Chain.t()
def async_draw(link, chainable), do: async_chain(chainable, link)
@doc ~S"""
do-notation enhanced with a `return` operation.
`return` is the simplest possible linking function, providing the correct `of/2`
instance for your monad.
## Examples
iex> monad [] do
...> [1, 2, 3]
...> end
[1, 2, 3]
iex> monad [] do
...> [1, 2, 3]
...> [4, 5, 6]
...> [7, 8, 9]
...> end
[
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> monad [] do
...> Witchcraft.Applicative.of([], 1)
...> end
[1]
iex> monad [] do
...> return 1
...> end
[1]
iex> monad [] do
...> monad {999} do
...> return 1
...> end
...> end
{1}
iex> monad [] do
...> a <- [1,2,3]
...> b <- [4,5,6]
...> return(a * b)
...> end
[
4, 5, 6,
8, 10, 12,
12, 15, 18
]
iex> monad [] do
...> a <- return 1
...> b <- return 2
...> return(a + b)
...> end
[3]
"""
defmacro monad(sample, do: input) do
returnized = desugar_return(input, sample)
Witchcraft.Chain.do_notation(returnized, &Witchcraft.Chain.chain/2)
end
@doc ~S"""
Variant of `monad/2` where each step internally occurs asynchonously, but lines
run strictly one after another.
## Examples
iex> async [] do
...> [1, 2, 3]
...> end
[1, 2, 3]
iex> async [] do
...> [1, 2, 3]
...> [4, 5, 6]
...> [7, 8, 9]
...> end
[
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> async [] do
...> Witchcraft.Applicative.of([], 1)
...> end
[1]
iex> async [] do
...> a <- [1,2,3]
...> b <- [4,5,6]
...> return(a * b)
...> end
[
4, 5, 6,
8, 10, 12,
12, 15, 18
]
iex> async [] do
...> a <- return 1
...> b <- return 2
...> return(a + b)
...> end
[3]
"""
defmacro async(sample, do: input) do
returnized = desugar_return(input, sample)
Witchcraft.Chain.do_notation(returnized, &Witchcraft.Monad.async_bind/2)
end
@doc false
# Convert `return`s to `of`s in the correct monadic context
def desugar_return(ast, sample) do
ast
|> Macro.prewalk(fn
{:monad = f, ctx, [inner_sample, inner_ast]} ->
{f, ctx, [inner_sample, desugar_return(inner_ast, inner_sample)]}
{{:., _, [_aliases, :monad]} = f, ctx, [inner_sample, inner_ast]} ->
{f, ctx, [inner_sample, desugar_return(inner_ast, inner_sample)]}
{:return, _ctx, [inner]} ->
quote do: Witchcraft.Applicative.of(unquote(sample), unquote(inner))
ast ->
ast
end)
end
end
definst Witchcraft.Monad, for: Function
definst Witchcraft.Monad, for: List
definst Witchcraft.Monad, for: Tuple do
use Witchcraft.Semigroup
import TypeClass.Property.Generator, only: [generate: 1]
custom_generator(_) do
{generate(""), generate("")}
end
end