Starting with enumerable helpers.

This commit is contained in:
James Harton 2020-11-16 09:52:18 +13:00
parent 7dec0917f7
commit 251106ad34
3 changed files with 158 additions and 0 deletions

View file

@ -1,8 +1,12 @@
defmodule Upshot do
alias Upshot.{Option, Result}
@moduledoc """
Upshot is a library for working with result types; either your own or
conventional ok/error tuples.
Currently experimental.
"""
@type t :: Option.t() | Result.t()
end

147
lib/upshot/result/enum.ex Normal file
View file

@ -0,0 +1,147 @@
defmodule Upshot.Result.Enum do
alias Upshot.{Result, Result.Proto}
@moduledoc """
Replacements or extensions for the standard library `Enum` functions which use
`Result`.
"""
@doc """
Removes any elements which contain ok results from the enumerable.
## Note
This function tolerates values which are not result types and does not remove
them from the resulting enumerable. Use `reject_oks!` if you want to ensure
there are no non-results in the collection.
## Examples
iex> [Result.ok(1), Result.error(2), Result.ok(3)]
...> |> reject_oks()
[{:error, 2}]
"""
@spec reject_oks(Enum.t()) :: Enum.t()
def reject_oks(enumerable),
do: Enum.reject(enumerable, &(Proto.valid?(&1) && Proto.ok?(&1)))
@doc """
Removes any elements which contain ok results from the enumerable, returning
only error results.
## Note
This function raises an `ArgumentError` if it encounters an element which is
not a result.
## Examples
iex> [Result.ok(1), Result.error(2), Result.ok(3)]
...> |> reject_oks!()
[{:error, 2}]
iex> [Result.ok(1), {}]
...> |> reject_oks!()
** (ArgumentError) Expected result, but got {}
"""
@spec reject_oks!(Enum.t()) :: Enum.t() | no_return
def reject_oks!(enumerable), do: Enum.reject(enumerable, &Result.ok?(&1))
@doc """
Removes any elements which contain error results from the enumerable.
## Note
This function tolerates values which are not result types and does not remove
them from the resulting enumerable.
## Examples
iex> [Result.ok(1), Result.error(2), Result.ok(3)]
...> |> reject_errors()
[{:ok, 1}, {:ok, 3}]
"""
@spec reject_errors(Enum.t()) :: Enum.t()
def reject_errors(enumerable),
do: Enum.reject(enumerable, &(Proto.valid?(&1) && Proto.error?(&1)))
@doc """
Removes any elements which contain error results from the enumerable.
## Note
This function raises an `ArgumentError` if it encounters an element which is
not a result.
## Examples
iex> [Result.ok(1), Result.error(2), Result.ok(3)]
...> |> reject_errors!()
[{:ok, 1}, {:ok, 3}]
iex> [Result.ok(1), {}]
...> |> reject_errors!()
** (ArgumentError) Expected result, but got {}
"""
@spec reject_errors!(Enum.t()) :: Enum.t()
def reject_errors!(enumerable),
do: Enum.reject(enumerable, &Result.error?(&1))
@doc """
Invokes `fun` for each element in `enumerable` with the accumulator.
Uses the first element of `enumerable` as the initial accumulator value.
Reduces as long as `fun` returns an ok result.
## Examples
iex> reduce_while_ok(1..5, &({:ok, &1 + &2}))
16
iex> reduce_while_ok(1..5, &(if &1 < 4, do: {:ok, &1 + &2}, else: {:error, &1}))
{:error, 4}
"""
@spec reduce_while_ok(Enum.t(), (any, any -> Result.t())) :: Result.t()
def reduce_while_ok(enumerable, fun) when is_function(fun) do
case Enum.take(enumerable, 1) do
[first] -> reduce_while_ok(enumerable, first, fun)
[] -> raise Enum.EmptyError, "empty error"
end
end
@doc """
Invokes `fun` for each element in `enumerable` with the accumulator starting
from `initial_value`.
Reduces as long as `fun` returns an ok result.
## Examples
iex> reduce_while_ok(1..5, 9, &({:ok, &1 + &2}))
24
iex> reduce_while_ok(1..5, 9, &(if &1 < 4, do: {:ok, &1 + &2}, else: {:error, &1}))
{:error, 4}
"""
@spec reduce_while_ok(Enum.t(), any, (any, any -> Result.t())) :: Result.t()
def reduce_while_ok(enumerable, initial_value, fun) when is_function(fun, 2) do
Enum.reduce_while(enumerable, initial_value, fn elem, acc ->
result =
fun
|> apply([elem, acc])
|> assert_valid!()
case Proto.unwrap(result) do
{:ok, acc} -> {:cont, acc}
{:error, _} = error -> {:halt, error}
end
end)
end
defp assert_valid!(result) do
if Proto.valid?(result),
do: result,
else: raise(ArgumentError, "Expected result, but got #{inspect(result)}")
end
end

View file

@ -0,0 +1,7 @@
defmodule Upshot.Result.EnumTest do
use ExUnit.Case, async: true
alias Upshot.Result
import Upshot.Result.Enum
doctest Upshot.Result.Enum
@moduledoc false
end