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 -> %Container) ---> %Container 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