lib/bunch/list.ex

defmodule Bunch.List do
  @moduledoc """
  A bunch of helper functions for list manipulation.
  """

  @doc """
  Generates a list using generator function and provided initial accumulator.

  Successive list elements are generated by calling `f` with the previous accumulator.
  The enumeration finishes when it returns `:halt` or `{:halt, accumulator}`.

  ## Examples

      iex> #{inspect(__MODULE__)}.unfoldr(10, fn 0 -> :halt; n -> {:cont, n-1} end)
      Enum.to_list(9..0)
      iex> f = fn
      ...>   <<size, content::binary-size(size), rest::binary>> -> {:cont, content, rest}
      ...>   binary -> {:halt, binary}
      ...> end
      iex> #{inspect(__MODULE__)}.unfoldr(<<2, "ab", 3, "cde", 4, "fghi">>, f)
      {~w(ab cde fghi), <<>>}
      iex> #{inspect(__MODULE__)}.unfoldr(<<2, "ab", 3, "cde", 4, "fg">>, f)
      {~w(ab cde), <<4, "fg">>}

  """
  @spec unfoldr(acc, (acc -> {:cont, a, acc} | {:cont, acc} | :halt | {:halt, acc})) ::
          [a] | {[a], acc}
        when acc: any, a: any
  def unfoldr(acc, f) do
    do_unfoldr(acc, f, [])
  end

  defp do_unfoldr(acc, f, res_acc) do
    case f.(acc) do
      {:cont, value, acc} -> do_unfoldr(acc, f, [value | res_acc])
      {:cont, acc} -> do_unfoldr(acc, f, [acc | res_acc])
      {:halt, acc} -> {Enum.reverse(res_acc), acc}
      :halt -> Enum.reverse(res_acc)
    end
  end

  @doc """
  The version of `unfoldr/2` that res_accepts `:ok` and `:error` return tuples.

  Behaves as `unfoldr/2` as long as `:ok` tuples are returned. Upon `:error`
  the processing is stopped and error is returned.

  ## Examples

      iex> f = fn
      iex>   <<a, b, rest::binary>> ->
      iex>     sum = a + b
      iex>     if rem(sum, 2) == 1, do: {:ok, {:cont, sum, rest}}, else: {:error, :even_sum}
      iex>   acc -> {:ok, {:halt, acc}}
      iex> end
      iex> #{inspect(__MODULE__)}.try_unfoldr(<<1,2,3,4,5>>, f)
      {:ok, {[3, 7], <<5>>}}
      iex> #{inspect(__MODULE__)}.try_unfoldr(<<2,4,6,8>>, f)
      {:error, :even_sum}

  """
  @spec try_unfoldr(
          acc,
          (acc ->
             {:ok, {:cont, a, acc} | {:cont, acc} | :halt | {:halt, acc}} | {:error, reason})
        ) ::
          {:ok, [a] | {[a], acc}} | {:error, reason}
        when acc: any, a: any, reason: any
  def try_unfoldr(acc, f) do
    do_try_unfoldr(acc, f, [])
  end

  defp do_try_unfoldr(acc, f, res_acc) do
    case f.(acc) do
      {:ok, {:cont, value, acc}} -> do_try_unfoldr(acc, f, [value | res_acc])
      {:ok, {:cont, acc}} -> do_try_unfoldr(acc, f, [acc | res_acc])
      {:ok, {:halt, acc}} -> {:ok, {Enum.reverse(res_acc), acc}}
      {:ok, :halt} -> {:ok, Enum.reverse(res_acc)}
      {:error, reason} -> {:error, reason}
    end
  end
end