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 overriding infix operators is 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/1) ...> ~> (&div(&1, 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/1) ...> ~> (&div(&1, 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/1) ...> ~>> (&(if &1 |> String.length() > 0, do: {:ok, &1 <> "!"}, else: {:error, :empty_string})) {:ok, "7!"} ``` ## Potential Changes My initial hope was to implement the operators as macros that would behave more similarily to `|>`. For example `{:ok, 1} ~> (&Integer.to_string/1)` could be written as `{:ok, 1} ~> Integer.to_string()`. Unfortunately it looks like this is infeasible using macros and in elixir but I might try again at some point. """ @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/1 {:ok, 6} iex> {:error, :reason} ...> ~> &Enum.sum/1 {:error, :reason} """ def value_tuple ~> function do value_tuple |> fmap(function) 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} """ def value_tuple ~>> tuple_function do value_tuple |> bind(tuple_function) end end