First pass.

This commit is contained in:
James Harton 2020-11-13 22:23:19 +13:00
parent cd1691fa51
commit 576ff84ca5
9 changed files with 567 additions and 16 deletions

View file

@ -1,18 +1,2 @@
defmodule Upshot do
@moduledoc """
Documentation for `Upshot`.
"""
@doc """
Hello world.
## Examples
iex> Upshot.hello()
:world
"""
def hello do
:world
end
end

View file

@ -0,0 +1,19 @@
defmodule Upshot.ExpectError do
defexception message: ""
alias Upshot.ExpectError
@moduledoc """
Raised when a call to `Result.expect/2` or `Option.expect/2` fails.
"""
@type t :: %ExpectError{
:__exception__ => true,
message: String.t()
}
@impl Exception
def exception(message) when is_binary(message), do: %ExpectError{message: message}
@impl Exception
def message(%ExpectError{message: message}), do: message
end

View file

@ -0,0 +1,19 @@
defmodule Upshot.UnwrapError do
defexception message: nil
alias Upshot.UnwrapError
@moduledoc """
Raised when a call to `Result.unwrap/1` or `Option.unwrap/1` fails.
"""
@type t :: %UnwrapError{
:__exception__ => true,
message: String.t()
}
@impl Exception
def exception(message), do: %UnwrapError{message: message}
@impl Exception
def message(%UnwrapError{message: message}), do: message
end

228
lib/upshot/option.ex Normal file
View file

@ -0,0 +1,228 @@
defmodule Upshot.Option do
alias Upshot.{ExpectError, Option.Proto, Result, UnwrapError}
@moduledoc """
A protocol for interacting with optional types.
"""
@type t :: Proto.t()
@type map_fn :: (any -> t)
@type predicate_fn :: (any -> boolean)
@doc """
Create a new option from value.
"""
@spec some(any) :: t
def some(value), do: {:ok, value}
@doc """
Create a new option.
"""
@spec none :: t
def none, do: :error
@doc """
Does the option contain a value?
"""
@spec some?(t) :: boolean
defdelegate some?(option), to: Proto
@doc """
Does the option contain no value?
"""
@spec none?(t) :: boolean
defdelegate none?(option), to: Proto
@doc """
Convert an option into a result.
If the option is some then it will contain the some, otherwise it will use the
provided error value.
"""
@spec ok_or(t, any) :: Result.t()
def ok_or(option, error) do
if Proto.some?(option),
do: option,
else: {:error, error}
end
@doc """
Convert an option into a result mapping a some value into an ok value, and
none into an error using the provided function.
"""
@spec ok_or_else(t, map_fn) :: t
def ok_or_else(option, err_callback) when is_function(err_callback, 1) do
if Proto.some?(option),
do: option,
else: safe_callback(option, err_callback)
end
@doc """
A logical and.
Returns none if the option is none, otherwise the other option.
"""
@spec and_(t, t) :: t
def and_(lhs, rhs) do
if Proto.none?(lhs),
do: lhs,
else: rhs
end
@doc """
Returns none, if the option is none, otherwise ok_callback with the wrapped
value and returns the result.
"""
@spec and_then(t, map_fn) :: t
def and_then(option, ok_callback) when is_function(ok_callback, 1) do
if Proto.none?(option),
do: option,
else: safe_callback(option, ok_callback)
end
@doc """
Returns none if the option is none, otherwise calls predicate with the wrapped
value returns the wrapped value if the predicate returns true, otherwise none.
"""
@spec filter(t, predicate_fn) :: t
def filter(option, predicate) do
case Proto.unwrap(option) do
{:ok, value} ->
if apply(predicate, [value]),
do: {:ok, value},
else: none()
:error ->
option
end
rescue
_error -> none()
end
@doc """
Returns the option if it contains a value, otherwise returns the alternative
option.
"""
@spec or_(t, t) :: t
def or_(lhs, rhs) do
if Proto.some?(lhs),
do: lhs,
else: rhs
end
@doc """
Returns the option if it contains a value, otherwise calls err_callback and
returns the result.
"""
@spec or_else(t, (() -> t)) :: t
def or_else(option, err_callback) when is_function(err_callback, 0) do
if Proto.some?(option) do
option
else
apply(err_callback, [])
end
rescue
_error -> none()
end
@doc """
Returns some if exactly one of the options are some, otherwise none.
"""
@spec xor_(t, t) :: t
def xor_(lhs, rhs) do
cond do
Proto.some?(lhs) && Proto.none?(rhs) -> lhs
Proto.none?(lhs) && Proto.some?(rhs) -> rhs
true -> none()
end
end
@doc """
Zips the two options together.
If both options are some, returns a some containing a tuple of both values, otherwise none.
"""
@spec zip(t, t) :: t
def zip(lhs, rhs) do
with {:ok, lhs} <- Proto.unwrap(lhs),
{:ok, rhs} <- Proto.unwrap(rhs) do
{:ok, {lhs, rhs}}
else
_error -> none()
end
end
@doc """
Unwraps the option and returns the value.
Raises an `ExpectError` on failure with `message`.
"""
@spec expect(t, String.t()) :: any | no_return
def expect(option, message) when is_binary(message) do
case Proto.unwrap(option) do
{:ok, value} -> value
:error -> raise ExpectError, message
end
end
@doc """
Unwraps the option presuming it's none.
Raises an `ExpectError` when the option is some with `message`.
"""
@spec expect_none(t, String.t()) :: t | no_return
def expect_none(option, message) when is_binary(message) do
if Proto.some?(option) do
raise ExpectError, message
else
none()
end
end
@doc """
Unwraps the option and returns the value.
Raises an `UnwrapError` on failure.
"""
@spec unwrap(t) :: any | no_return
def unwrap(option) do
case Proto.unwrap(option) do
{:ok, value} -> value
:error -> raise UnwrapError, "Attempt to unwrap a none"
end
end
@doc """
Unwraps an option and returns a none.
Raises an `UnwrapError` on failure.
"""
@spec unwrap_none(t) :: t | no_return
def unwrap_none(option) do
if Proto.none?(option) do
option
else
raise UnwrapError, "Attempt to unwrap a some"
end
end
@doc """
Unwraps an option and returns it's contained value or a default.
"""
@spec unwrap_or_default(t, any) :: any
def unwrap_or_default(option, default) do
case Proto.unwrap(option) do
{:ok, value} -> value
:error -> default
end
end
# Safely execute a mapping function, catching an exceptions and converting
# them into a result.
@spec safe_callback(t, map_fn) :: Result.t()
defp safe_callback(option, callback) when is_function(callback, 1) do
apply(callback, [option])
rescue
error -> {:error, error}
end
end

View file

@ -0,0 +1,16 @@
defprotocol Upshot.Option.Proto do
@moduledoc """
A protocol for interacting with optional types.
"""
@fallback_to_any true
@spec some?(t) :: boolean
def some?(_option)
@spec none?(t) :: boolean
def none?(_option)
@spec unwrap(t) :: {:ok, any} | :error
def unwrap(_option)
end

View file

@ -0,0 +1,26 @@
defimpl Upshot.Option.Proto, for: Any do
@moduledoc """
Implements the option protocol for arbitrary terms.
"""
@doc """
Returns `true` for `{:ok, value}` tuples, otherwise `false`.
"""
@spec some?(any) :: boolean
def some?({:ok, _value}), do: true
def some?(_option), do: false
@doc """
Returns `false` for `{:ok, value}` tuples, otherwise `true`.
"""
@spec none?(any) :: boolean
def none?({:ok, _value}), do: false
def none?(_option), do: true
@doc """
Returns the internal value of `{:ok, value}` or `:error`.
"""
@spec unwrap(any) :: {:ok, any} | :error
def unwrap({:ok, value}), do: value
def unwrap(_option), do: :error
end

210
lib/upshot/result.ex Normal file
View file

@ -0,0 +1,210 @@
defmodule Upshot.Result do
alias Upshot.{ExpectError, Option, Result.Proto, UnwrapError}
@moduledoc """
Functions for working with types which implement the result protocol.
"""
@type t :: Proto.t()
@type map_fn :: (any -> {:ok, any} | {:error, any})
@doc """
Create a new result with an ok value.
"""
@spec ok(any) :: t
def ok(value), do: {:ok, value}
@doc """
Create a new result with an error value.
"""
@spec error(any) :: t
def error(reason), do: {:error, reason}
@doc """
Convert a result into an option.
"""
@spec to_option(t) :: Option.t()
def to_option(result) do
case Proto.unwrap(result) do
{:ok, value} -> {:ok, value}
_error -> :error
end
end
@doc """
Is the result okay?
"""
@spec ok?(t) :: boolean
defdelegate ok?(result), to: Proto
@doc """
Is the result an error?
"""
@spec error?(t) :: boolean
defdelegate error?(result), to: Proto
@doc """
Returns true if the result is ok and it's value equals the test value.
"""
@spec contains?(t, any) :: boolean
def contains?(result, test_value), do: Proto.unwrap(result) == {:ok, test_value}
@doc """
Returns true if the result is an error and it's value equals the test value.
"""
@spec contains_err?(t, any) :: boolean
def contains_err?(result, test_value), do: Proto.unwrap(result) == {:error, test_value}
@doc """
Maps an ok result into a new result using the provided function, leaving an
error result untouched.
"""
@spec map(t, map_fn) :: t
def map(result, ok_callback) when is_function(ok_callback, 1) do
with {:ok, value} <- Proto.unwrap(result),
{:ok, value} <- safe_callback(value, ok_callback) do
{:ok, value}
else
_error -> result
end
end
@doc """
Maps an ok result into a new result using the provided function, replacing an
error value with the default.
"""
@spec map_or(t, map_fn, any) :: t
def map_or(result, ok_callback, default) when is_function(ok_callback, 1) do
with {:ok, value} <- Proto.unwrap(result),
{:ok, value} <- safe_callback(value, ok_callback) do
{:ok, value}
else
_error -> {:ok, default}
end
end
@doc """
Maps a result to another result by applying `ok_callback` or `err_callback`
depending on whether the result is successful or not.
"""
@spec map_or_else(t, map_fn, map_fn) :: t
def map_or_else(result, ok_callback, err_callback) do
case Proto.unwrap(result) do
{:ok, value} -> safe_callback(value, ok_callback)
{:error, value} -> safe_callback(value, err_callback)
end
end
@doc """
Maps an error result into another result by applying the function to an error
value and leaving an ok value untouched.
"""
@spec map_err(t, map_fn) :: t
def map_err(result, err_callback) do
case Proto.unwrap(result) do
{:error, value} -> safe_callback(value, err_callback)
_other -> result
end
end
@doc """
A logical and.
If `lhs` is ok, returns `rhs`, otherwise returns `lhs`.
"""
@spec and_(t, t) :: t
def and_(lhs, rhs) do
case Proto.unwrap(lhs) do
{:ok, _value} -> rhs
_other -> lhs
end
end
@doc """
Calls `ok_callback` if the result is ok, otherwise returns the error value.
"""
@spec and_then(t, map_fn) :: t
def and_then(result, ok_callback) when is_function(ok_callback, 1) do
case Proto.unwrap(result) do
{:ok, value} -> safe_callback(value, ok_callback)
_other -> result
end
end
@doc """
A logical or.
If `lhs` is an error, returns `rhs`, otherwise returns `lhs`.
"""
@spec or_(t, t) :: t
def or_(lhs, rhs) do
case Proto.unwrap(lhs) do
{:error, _value} -> rhs
_ -> lhs
end
end
@doc """
Unwraps the result and returns the value.
Raises an `ExpectError` on failure with `message`.
"""
@spec expect(t, String.t()) :: any | no_return
def expect(result, message) when is_binary(message) do
case Proto.unwrap(result) do
{:ok, value} ->
value
{:error, _} ->
raise ExpectError, message
end
end
@doc """
Unwraps the result and returns the value.
Raises an `UnwrapError` on failure.
"""
@spec unwrap(t) :: any | no_return
def unwrap(result) do
case Proto.unwrap(result) do
{:ok, value} -> value
{:error, _} -> raise UnwrapError, "Attempt to unwrap error result"
end
end
@doc """
Returns the contained ok value, or a provided default.
"""
@spec unwrap_or(t, any) :: any
def unwrap_or(result, default) do
case Proto.unwrap(result) do
{:ok, value} -> value
_other -> default
end
end
@doc """
Returns the contained ok value, or computes it with the provided function.
"""
@spec unwrap_or_else(t, map_fn) :: any
def unwrap_or_else(result, err_callback) when is_function(err_callback, 1) do
case Proto.unwrap(result) do
{:ok, value} ->
value
{:error, value} ->
value
|> safe_callback(err_callback)
|> unwrap()
end
end
# Calls a callback, but rescues exceptions and converts them results.
@spec safe_callback(any, map_fn) :: t
defp safe_callback(value, callback) do
apply(callback, [value])
rescue
exception -> {:error, exception}
end
end

View file

@ -0,0 +1,18 @@
defprotocol Upshot.Result.Proto do
@moduledoc """
The result protocol.
Implement this for your own result types, as needed.
"""
@fallback_to_any true
@spec ok?(t) :: boolean
def ok?(_result)
@spec error?(t) :: boolean
def error?(_result)
@spec unwrap(t) :: {:ok, any} | {:error, any}
def unwrap(_result)
end

View file

@ -0,0 +1,31 @@
defimpl Upshot.Result.Proto, for: Any do
@moduledoc """
Implements the result protocol for arbitrary terms.
"""
@doc """
Returns `true` for `{:ok, value}` and `:ok`, otherwise `false`.
"""
@spec ok?(any) :: boolean
def ok?({:ok, _value}), do: true
def ok?(:ok), do: true
def ok?(_result), do: false
@doc """
Returns `true` for `{:error, reason}` and `:error`.
"""
@spec error?(any) :: boolean
def error?({:error, _reason}), do: true
def error?(%{:__exception__ => true}), do: true
def error?(_result_), do: true
@doc """
Unwrap a result, if possible.
Returns the value of an `{:ok, value}` tuple, otherwise `{:error, reason}`.
"""
@spec unwrap(any) :: {:ok, any} | {:error, any}
def unwrap({:ok, value}), do: {:ok, value}
def unwrap({:error, value}), do: {:error, value}
def unwrap(%{:__exception__ => true} = error), do: {:error, error}
end