lib/fex.ex

defmodule Fex do
  @moduledoc """
  Documentation for `Fex`.

  This is an experimental package - do not use in production!

  Many Elixir functions are returning two types of tuples:
  - {:ok, data}
  - {:error, error}

  Together they maps quite closely to "either" structure.
  Idea is to instead of using the `case` function(s) to
  check was operation successfull:

  ```
    with {:ok, json} <- File.read("list.json"),
         {:ok, data} <- Jason.decode(json)
    do

      data
      |> Enum.sum()

    else
        Logger.error("Error occurred")
        IO.inspect(msg)
        0
    end
  ```

  we could use functions that are operating on
  "either" looking structures:

  ```
    File.read("list.json") do
    |> Fex.chain(&Jason.decode/1)
    |> Fex.apply(&Enum.sum/1)
    |> Fex.fold(&(&1), fn msg ->
        Logger.error("Error occurred")
        IO.inspect(msg)
        0
    end)
  ```

  Above approach gives some advatanges over the
  "usual" approach:
  - developer has freedom to decide when to handle
    the "error" case, which makes it easier to divide
    code to dirty and pure parts
  - error cases can be grouped together
  - code is easier to reason about with a little bit
    of understanding about the "either" idea
  """

  @doc """
  Maps list of results using `Enum.map/2`

  ## Examples

      iex> Fex.map({:ok, [1, 2]}, &(&1 + 2))
      {:ok, [3, 4]}

      iex> Fex.map({:error, "Error message"}, &(&1 + 2))
      {:error, "Error message"}
  """
  def map({:ok, data}, iteration_fn) do
    {:ok, Enum.map(data, iteration_fn)}
  end

  def map({:error, error}, _iteration_fn), do: {:error, error}

  @doc """
  Applies function over data

  ## Examples

      iex> Fex.apply({:ok, [1, 2]}, &Enum.sum/1)
      {:ok, 3}

      iex> Fex.map({:error, "Error message"}, &Enum.sum/1)
      {:error, "Error message"}
  """
  def apply({:ok, data}, function) do
    {:ok, function.(data)}
  end

  def apply({:error, error}, _function), do: {:error, error}

  @doc """
  Like apply but doesn't wrap function in the "either" format.
  It relies on the fact that applied function will return the
  "either" format.

  Useful when you have an "either" structure already and you
  need to pass it to function that returns another "either"
  structure(using apply would cause "nesting" of eithers).

  ## Examples

      iex> Fex.chain({:ok, "{}"}, &Jason.decode/1)
      {:ok, %{}}

      iex> Fex.chain({:error, "Error message"}, &Jason.decode/1)
      {:error, "Error message"}
  """
  def chain({:ok, data}, function), do: function.(data)
  def chain({:error, error}, _function), do: {:error, error}

  @doc """
  When you would like to extract the pure value out of the "either"
  struct.

  ## Examples

      iex> Fex.fold({:ok, 5}, &(&1), &(&1))
      5

      iex> Fex.fold({:error, "Error message"},  &(&1), &(&1))
      "Error message"
  """
  def fold({:ok, data}, success_fn, _error_fn), do: success_fn.(data)
  def fold({:error, error}, _success_fn, error_fn), do: error_fn.(error)
end