defmodule Monok do @moduledoc """ #### _Monad on :ok_ Provides the infix pipe operators `~>`, `~>>`, and `<~>` for writing clean pipelines that treat `{:ok, result}` and `{:error, reason}` tuples like functors, monads or applicatives. Also provides the functions `fmap`, `bind` and `lift` as which are functionally identical but are less cryptic and can be used without overriding any inifix operators which could potentially conflict with other libraries. ## Why would you ever do this? Whilst writing unnecessary macros and overriding infix operators are both generally considered bad practice I thought I'd try this out given just how freqently `{:ok, result}` and `{:error, reason}` tuples are encountered in Elixir. ## Functor Pipelines Allows you to write clean pipelines that transforms values inside of `{:ok, value}` tuples. ``` iex> {:ok, [1, 2, 3]} ...> ~> Enum.sum() ...> ~> div(2) {:ok, 3} ``` If the input is an `{:error, reason}` tuple it is carried through the pipeline without applying any transformations. ``` iex> {:error, :reason} ...> ~> Enum.sum() ...> ~> div(2) {:error, :reason} ``` ## Monad Pipelines Allows you to write clean pipelines that transform values in `{:ok, value}` tuples with functions that also return `{:ok, value}` tuples. ``` iex> decrement = fn ...> x when x > 0 -> {:ok, x - 1} ...> _ -> {:error, :input_too_small} ...> end iex> {:ok, 3} ...> ~>> decrement.() ...> ~>> decrement.() {:ok, 1} ``` If at any point in the pipeline an `{:error, reason}` tuple is returned it is carried through without any of the transformation functions being applied. ``` iex> decrement = fn ...> x when x > 0 -> {:ok, x - 1} ...> _ -> {:error, :input_too_small} ...> end iex> ...> {:ok, 3} ...> ~>> (fn _ -> {:error, :contrived_example} end).() ...> ~>> decrement.() ...> ~>> decrement.() {:error, :contrived_example} ``` ## Mixed Pipelines These pipe operators don't have to be used in seperate pipelines but can be used together or even with the `|>` standard pipe operator. ``` iex> 7 ...> |> (&(if &1 > 5, do: {:ok, &1}, else: {:error, :too_low})).() ...> ~> Integer.to_string() ...> ~>> (&(if &1 |> String.length() > 0, do: {:ok, &1 <> "!"}, else: {:error, :empty_string})).() {:ok, "7!"} ``` """ @doc """ Applies a function to a value wrapped in an ok tuple, has no effect if given an error tuple. ## Examples iex> {:ok, [1, 2, 3]} ...> |> Monok.fmap(&Enum.sum/1) {:ok, 6} iex> {:error, :reason} ...> |> Monok.fmap(&Enum.sum/1) {:error, :reason} """ def fmap(value_tuple, function) def fmap({:ok, value}, function) do {:ok, function.(value)} end def fmap({:error, reason}, _function) do {:error, reason} end @doc """ Applies a function wrapped in an :ok tuple to a value wrapped in an :ok tuple. Carries through an :error tuple if either the value or function arguments are given as :error tuples instead of :ok tuples. ## Examples iex> {:ok, [1, 2, 3]} ...> |> Monok.lift({:ok, &Enum.sum/1}) {:ok, 6} iex> {:ok, 1} ...> |> Monok.lift({:error, :reason}) {:error, :reason} iex> {:error, :reason} ...> |> Monok.lift({:ok, &Enum.sum/1}) {:error, :reason} """ def lift(value_tuple, function_tuple) def lift({:ok, value}, {:ok, function}) do {:ok, function.(value)} end def lift({:error, reason}, _function_tuple) do {:error, reason} end def lift(_value_tuple, {:error, reason}) do {:error, reason} end @doc """ Applies a function that returns a value wrapped in an :ok tuple to a value wrapped in an :ok tuple. Carries through an :error tuple if either the value argument is given as an :error tuple or the function returns an :error tuple when applied to the value. ## Examples iex> {:ok, [1, 2, 3]} ...> |> Monok.bind(fn x -> {:ok, Enum.sum(x)} end) {:ok, 6} iex> {:ok, [1, 2, 3]} ...> |> Monok.bind(fn _ -> {:error, :reason} end) {:error, :reason} iex> {:error, :reason} ...> |> Monok.bind(fn x -> {:ok, Enum.sum(x)} end) {:error, :reason} """ def bind(value_tuple, function) def bind({:ok, value}, function) do function.(value) end def bind({:error, reason}, _function) do {:error, reason} end @doc """ Infix fmap operator. Treats input as value_tuple a functor. Applies a function to a value wrapped in an ok tuple, has no effect if given an error tuple. ## Examples iex> {:ok, [1, 2, 3]} ...> ~> Enum.sum() {:ok, 6} iex> {:error, :reason} ...> ~> Enum.sum() {:error, :reason} """ defmacro quote_value_tuple ~> {function, metadata, call_args} do quote generated: true do case unquote(quote_value_tuple) do {:ok, value} -> {:ok, unquote({function, metadata, [quote(do: value) | call_args]})} other -> other end end end @doc """ Infix lift operator. Treats the function_tuple as an applicative. Applies a function wrapped in an :ok tuple to a value wrapped in an :ok tuple. Carries through an :error tuple if either the value or function arguments are given as :error tuples instead of :ok tuples. ## Examples iex> {:ok, [1, 2, 3]} ...> <~> {:ok, &Enum.sum/1} {:ok, 6} iex> {:ok, 1} ...> <~> {:error, :reason} {:error, :reason} iex> {:error, :reason} ...> <~> {:ok, &Enum.sum/1} {:error, :reason} """ def value_tuple <~> function_tuple do value_tuple |> lift(function_tuple) end @doc """ Infix bind operator. Treats the value_tuple and tuple_function as monads. Applies a function that returns a value wrapped in an :ok tuple to a value wrapped in an :ok tuple. Carries through an :error tuple if either the value argument is given as an :error tuple or the function returns an :error tuple when applied to the value. ## Examples iex> {:ok, [1, 2, 3]} ...> ~>> (fn x -> {:ok, Enum.sum(x)} end).() {:ok, 6} iex> {:ok, [1, 2, 3]} ...> ~>> (fn _ -> {:error, :reason} end).() {:error, :reason} iex> {:error, :reason} ...> ~>> (fn x -> {:ok, Enum.sum(x)} end).() {:error, :reason} """ defmacro quote_value_tuple ~>> {function, metadata, call_args} do quote generated: true do case unquote(quote_value_tuple) do {:ok, value} -> unquote({function, metadata, [quote(do: value) | call_args]}) other -> other end end end end