lib/quark/compose.ex

defmodule Quark.Compose do
  @moduledoc ~S"""
  Function composition is taking two functions, and joining them together to
  create a new function. For example:

  ## Examples

      iex> sum_plus_one = compose([&(&1 + 1), &Enum.sum/1])
      ...> sum_plus_one.([1,2,3])
      7

  In this case, we have joined `Enum.sum` with a function that adds one,
  to create a new function that takes a list, sums it, and adds one.

  Note that composition normally applies _from right to left_, though `Quark`
  provides the opposite in the form of `*_forward` functions.
  """

  import Quark.SKI

  import Quark.Curry

  @doc ~S"""
  Function composition

  ## Examples

      iex> sum_plus_one = compose(&(&1 + 1), &Enum.sum/1)
      ...> [1, 2, 3] |> sum_plus_one.()
      7

  """
  @spec compose(fun, fun) :: any
  def compose(g, f) do
    fn x ->
      x
      |> curry(f).()
      |> curry(g).()
    end
  end

  @doc ~S"""
  Function composition, from the tail of the list to the head

  ## Examples

      iex> sum_plus_one = compose([&(&1 + 1), &Enum.sum/1])
      ...> [1,2,3] |> sum_plus_one.()
      7

  """
  @spec compose([fun]) :: fun
  def compose(funcs) when is_list(funcs), do: funcs |> List.foldr(&id/1, &compose/2)

  @doc ~S"""
  Function composition, from the head to tail (left-to-right)

  ## Examples

      iex> sum_plus_one = compose_forward(&Enum.sum/1, &(&1 + 1))
      ...> [1, 2, 3] |> sum_plus_one.()
      7

  """
  @spec compose_forward(fun, fun) :: fun
  def compose_forward(f, g) do
    compose(g, f)
  end

  @doc ~S"""
  Infix "forward" compositon operator

  ## Examples

      iex> sum_plus_one = (&Enum.sum/1) <~> fn x -> x + 1 end
      ...> sum_plus_one.([1, 2, 3])
      7

      iex> x200 = (&(&1 * 2)) <~> (&(&1 * 10)) <~> (&(&1 * 10))
      ...> x200.(5)
      1000

      iex> add_one  = &(&1 + 1)
      ...> piped    = [1, 2, 3] |> Enum.sum() |> add_one.()
      ...> composed = [1, 2, 3] |> ((&Enum.sum/1) <~> add_one).()
      ...> piped == composed
      true

  """
  @spec fun <~> fun :: fun
  def f <~> g, do: compose_forward(f, g)

  @doc ~S"""
  Compose functions, from the head of the list of functions.

  ## Examples

      iex> sum_plus_one = compose_forward([&Enum.sum/1, &(&1 + 1)])
      ...> sum_plus_one.([1, 2, 3])
      7

  """
  @spec compose_forward([fun]) :: fun
  def compose_forward(funcs) when is_list(funcs), do: funcs |> List.foldl(&id/1, &compose/2)
end