defmodule Needlework do @moduledoc """ Needlework brings additional operators to Elixir that allows you to "thread" results of your functions into other function calls. Basically extending the `Kernel.|>/2` operator. Just `use Needlework` in your modules and thread away! Example: ``` defmodule MyModule do use Needlework @spec foo(func :: fun()) :: list() def foo(func) do func ~> Enum.map([1, 2, 3]) end end ``` """ defmacro __using__(_) do quote do import Needlework, only: :macros end end @type ok :: {:ok, any()} @type error :: {:error, any()} @doc """ Wraps the value in `t:Needlework.ok/0` tuple. Example: iex> 5 |> Needlework.ok_unit() {:ok, 5} iex> {:ok, 5} |> Needlework.ok_unit() {:ok, 5} iex> {:error, ""} |> Needlework.ok_unit() {:error, ""} """ @spec ok_unit(any) :: {:ok | :error, any} def ok_unit({:ok, _} = value), do: value def ok_unit({:error, _} = value), do: value def ok_unit(value), do: {:ok, value} @doc """ Bind operator. If value on the left is a plain value -> converts it to `t:Needlework.ok/0` | `t:Needlework.error/0` tuple then desctructures the tuple. If it was `t:Needlework.ok/0` tuple -> passes the value for evaluation. If it was `t:Needlework.ok/0` tuple -> skips the evaluation Example: iex> import Needlework iex> foo = fn x -> {:ok, x * 2} end iex> 2 <|> foo.() <|> foo.() <|> foo.() {:ok, 16} iex> bar = fn _ -> {:error, "impossible"} end iex> 2 <|> foo.() <|> bar.() <|> foo.() {:error, "impossible"} """ defmacro left <|> right do quote do unquote(left) |> Needlework.ok_unit() |> (fn {:ok, value} -> value |> unquote(right) {:error, reason} -> {:error, reason} end).() end end @doc """ Same as `Needlework.<|>/2` but places the value instead of `_`. If no `_` present works like a `Needlework.<|>/2` Examples: iex> import Needlework iex> foo = fn x, y -> {:ok, x ++ y} end iex> [1, 2, 3] <~> foo.(_, [1, 2, 3]) <~> foo.([4, 5, 6], _) {:ok, [4, 5, 6, 1, 2, 3, 1, 2, 3]} iex> [1, 2, 3] <~> foo.([1, 2, 3]) <~> foo.([4, 5, 6]) {:ok, [1, 2, 3, 1, 2, 3, 4, 5, 6]} iex> bar = fn _, _ -> {:error, "reason"} end iex> [1, 2, 3] <~> bar.([1, 2, 3]) <~> foo.([4, 5, 6]) {:error, "reason"} """ defmacro left <~> right do quote do unquote(left) |> Needlework.ok_unit() |> (fn {:ok, value} -> value ~>> unquote(right) {:error, reason} -> {:error, reason} end).() end end @doc """ Allows to thread the value on the left as the last argument Example: iex> import Needlework iex> [1, 2, 3] ~> Kernel.++([4, 5, 6]) [4, 5, 6, 1, 2, 3] iex> fn x -> x*2 end ~> Enum.map([1, 2, 3]) [2, 4, 6] """ defmacro left ~> right do new_right = add_last_arg(right, left) quote do unquote(new_right) end end @doc """ Allows to thread the value on the left to a specific spot on the right. Value from the left will be placed instead of `_`. If no `_` present works like a regular `Kernel.|>/2` Example: iex> import Needlework iex> [1, 2, 3] ~>> Kernel.++([4, 5, 6], _) [4, 5, 6, 1, 2, 3] iex> [1, 2, 3] ~>> Kernel.++([4, 5, 6]) iex> [] ~>> Enum.reduce([1, 2, 3], _, fn x, acc -> [x | acc] end) [3, 2, 1] """ defmacro left ~>> right do case replace_underscore_args(right, left) do {:ok, new_args} -> new_right = put_elem(right, 2, new_args) quote do unquote(new_right) end {:error, _} -> quote do unquote(left) |> unquote(right) end end end defp replace_underscore_args({_, _, args}, replacement) do Enum.reduce(args, {:error, []}, fn {:_, _, _}, {_, args} -> {:ok, args ++ [replacement]} val, {res, args} -> {res, args ++ [val]} end) end defp add_last_arg({name, context, args}, argument), do: {name, context, args ++ [argument]} end