Current section

Files

Jump to
funx lib monad either.ex
Raw

lib/monad/either.ex

defmodule Funx.Monad.Either do
@moduledoc """
The `Funx.Monad.Either` module provides an implementation of the `Either` monad, a functional abstraction used to model computations that may fail.
An `Either` represents one of two possibilities:
- `Right(value)`: a successful result
- `Left(error)`: a failure or error
This pattern is commonly used in place of exceptions to handle errors explicitly and safely in functional pipelines.
### Constructors
- `right/1`: Wraps a value in the `Right` branch.
- `left/1`: Wraps a value in the `Left` branch.
- `pure/1`: Alias for `right/1`.
### Refinement
- `right?/1`: Returns `true` if the value is a `Right`.
- `left?/1`: Returns `true` if the value is a `Left`.
### Fallback and Extraction
- `get_or_else/2`: Returns the value from a `Right`, or a default if `Left`.
- `or_else/2`: Returns the original `Right`, or invokes a fallback function if `Left`.
- `map_left/2`: Transforms a `Left` using a function, leaving `Right` values unchanged.
- `flip/1`: Swaps `Left` and `Right`, turning errors into successes and vice versa.
- `filter_or_else/3`: Applies a predicate to the `Right` value; if false, returns a fallback `Left`.
### List Operations
- `concat/1`: Removes all `Left` values and unwraps the `Right` values from a list.
- `concat_map/2`: Applies a function and collects only `Right` results.
- `sequence/1`: Converts a list of `Either` values into a single `Either` of list.
- `traverse/2`: Applies a function to each element in a list and sequences the results.
- `sequence_a/1`: Like `sequence/1`, but accumulates all errors from `Left` values.
- `traverse_a/2`: Like `traverse/2`, but accumulates all `Left` values instead of short-circuiting.
- `wither_a/2`: Like `traverse_a/2`, but filters out `Nothing` results and collects only `Just` values.
### Validation
- `validate/2`: Applies multiple validators to a single input, collecting all errors.
### Lifting
- `lift_predicate/3`: Turns a predicate into an `Either`, returning `Right` on `true` and `Left` on `false`.
- `lift_maybe/2`: Converts a `Maybe` to an `Either` using a fallback value.
- `lift_eq/1`: Lifts an equality function into the `Either` context.
- `lift_ord/1`: Lifts an ordering function into the `Either` context.
## Transformation
- `map_left/2` – Transforms the error inside a `Left`, leaving `Right` values untouched.
### Elixir Interoperability
- `from_result/1`: Converts `{:ok, val}` or `{:error, err}` into an `Either`.
- `to_result/1`: Converts an `Either` into a result tuple.
- `from_try/1`: Runs a function and returns `Right` on success or `Left` on exception.
- `to_try!/1`: Unwraps a `Right`, or raises an error from a `Left`.
## Protocols
The `Left` and `Right` structs implement the following protocols, making the `Either` abstraction composable and extensible:
- `Funx.Eq`: Enables equality comparisons between `Either` values.
- `Funx.Foldable`: Implements `fold_l/3` and `fold_r/3` for reducing over contained values.
- `Funx.Monad`: Provides `map/2`, `ap/2`, and `bind/2` for monadic composition.
- `Funx.Ord`: Defines ordering behavior for comparing `Left` and `Right` values.
Although these implementations are defined on each constructor (`Left` and `Right`), the behavior is consistent across the `Either` abstraction.
This module helps you model failure explicitly, compose error-aware logic, and integrate cleanly with Elixir's functional idioms.
"""
import Funx.Appendable, only: [append: 2, coerce: 1]
import Funx.Monad, only: [map: 2]
import Funx.Foldable, only: [fold_l: 3]
alias Funx.Eq
alias Funx.Ord
alias Funx.Monad.{Either, Maybe}
alias Either.{Left, Right}
alias Maybe.{Just, Nothing}
@type t(left, right) :: Left.t(left) | Right.t(right)
@doc """
Wraps a value in the `Right` monad.
## Examples
iex> Funx.Monad.Either.right(5)
%Funx.Monad.Either.Right{right: 5}
"""
@spec right(any()) :: Right.t(any())
def right(value), do: Right.pure(value)
@doc """
Wraps a value in the `Left` monad.
## Examples
iex> Funx.Monad.Either.left("error")
%Funx.Monad.Either.Left{left: "error"}
"""
@spec left(any()) :: Left.t(any())
def left(value), do: Left.pure(value)
@doc """
Alias for `right/1`.
## Examples
iex> Funx.Monad.Either.pure(2)
%Funx.Monad.Either.Right{right: 2}
"""
@spec pure(any()) :: Right.t(any())
def pure(value), do: right(value)
@doc """
Returns `true` if the `Either` is a `Left` value.
## Examples
iex> Funx.Monad.Either.left?(Funx.Monad.Either.left("error"))
true
iex> Funx.Monad.Either.left?(Funx.Monad.Either.right(5))
false
"""
@spec left?(t(any(), any())) :: boolean()
def left?(%Left{}), do: true
def left?(_), do: false
@doc """
Returns `true` if the `Either` is a `Right` value.
## Examples
iex> Funx.Monad.Either.right?(Funx.Monad.Either.right(5))
true
iex> Funx.Monad.Either.right?(Funx.Monad.Either.left("error"))
false
"""
@spec right?(t(any(), any())) :: boolean()
def right?(%Right{}), do: true
def right?(_), do: false
@doc """
Filters the value inside a `Right` using the given `predicate`. If the predicate returns `false`,
a `Left` is returned using the `left_func`.
## Examples
iex> Funx.Monad.Either.filter_or_else(Funx.Monad.Either.right(5), fn x -> x > 3 end, fn -> "error" end)
%Funx.Monad.Either.Right{right: 5}
iex> Funx.Monad.Either.filter_or_else(Funx.Monad.Either.right(2), fn x -> x > 3 end, fn -> "error" end)
%Funx.Monad.Either.Left{left: "error"}
"""
@spec filter_or_else(t(any(), any()), (any() -> boolean()), (-> any())) :: t(any(), any())
def filter_or_else(either, predicate, left_func) do
fold_l(
either,
fn value ->
if predicate.(value) do
either
else
left(left_func.())
end
end,
fn _left_value -> either end
)
end
@doc """
Retrieves the value from a `Right`, returning the `default` value if `Left`.
## Examples
iex> Funx.Monad.Either.get_or_else(Funx.Monad.Either.right(5), 0)
5
iex> Funx.Monad.Either.get_or_else(Funx.Monad.Either.left("error"), 0)
0
"""
@spec get_or_else(t(any(), any()), any()) :: any()
def get_or_else(either, default) do
fold_l(
either,
fn value -> value end,
fn _left_value -> default end
)
end
@doc """
Returns the current `Right` value or invokes the `fallback_fun` if `Left`.
Useful for recovering from a failure by providing an alternate computation.
## Examples
iex> Funx.Monad.Either.or_else(Funx.Monad.Either.left("error"), fn -> Funx.Monad.Either.right(42) end)
%Funx.Monad.Either.Right{right: 42}
iex> Funx.Monad.Either.or_else(Funx.Monad.Either.right(10), fn -> Funx.Monad.Either.right(42) end)
%Funx.Monad.Either.Right{right: 10}
"""
@spec or_else(t(error, value), (-> t(error, value))) :: t(error, value)
when error: term(), value: term()
def or_else(%Left{}, fallback_fun) when is_function(fallback_fun, 0), do: fallback_fun.()
def or_else(%Right{} = right, _fallback_fun), do: right
@doc """
Swaps the `Left` and `Right` branches of the `Either`.
Turns a `Left` into a `Right` and vice versa, preserving the contained term.
## Examples
iex> Funx.Monad.Either.flip(Funx.Monad.Either.left(:error))
%Funx.Monad.Either.Right{right: :error}
iex> Funx.Monad.Either.flip(Funx.Monad.Either.right(42))
%Funx.Monad.Either.Left{left: 42}
"""
@spec flip(t(left, right)) :: t(right, left)
when left: term(), right: term()
def flip(%Left{left: l}), do: %Right{right: l}
def flip(%Right{right: r}), do: %Left{left: r}
@doc """
Lifts an equality function to compare `Either` values:
- `Right` vs `Right`: Uses the custom equality function.
- `Left` vs `Left`: Uses the custom equality function.
- `Left` vs `Right` or vice versa: Always `false`.
## Examples
iex> eq = Funx.Monad.Either.lift_eq(%{
...> eq?: fn x, y -> x == y end,
...> not_eq?: fn x, y -> x != y end
...> })
iex> eq.eq?.(Funx.Monad.Either.right(5), Funx.Monad.Either.right(5))
true
iex> eq.eq?.(Funx.Monad.Either.right(5), Funx.Monad.Either.right(10))
false
iex> eq.eq?.(Funx.Monad.Either.left(:a), Funx.Monad.Either.left(:a))
true
iex> eq.eq?.(Funx.Monad.Either.left(:a), Funx.Monad.Either.left(:b))
false
iex> eq.eq?.(Funx.Monad.Either.right(5), Funx.Monad.Either.left(:a))
false
"""
@spec lift_eq(Eq.Utils.eq_t()) :: Eq.Utils.eq_map()
def lift_eq(custom_eq) do
custom_eq = Eq.Utils.to_eq_map(custom_eq)
%{
eq?: fn
%Right{right: v1}, %Right{right: v2} -> custom_eq.eq?.(v1, v2)
%Left{left: v1}, %Left{left: v2} -> custom_eq.eq?.(v1, v2)
%Left{}, %Right{} -> false
%Right{}, %Left{} -> false
end,
not_eq?: fn
%Right{right: v1}, %Right{right: v2} -> custom_eq.not_eq?.(v1, v2)
%Left{left: v1}, %Left{left: v2} -> custom_eq.not_eq?.(v1, v2)
%Left{}, %Right{} -> true
%Right{}, %Left{} -> true
end
}
end
@doc """
Creates a custom ordering function for `Either` values using the provided `custom_ord`.
The `custom_ord` must be a map with `:lt?`, `:le?`, `:gt?`, and `:ge?` functions. These are used to compare the internal `left` or `right` values.
## Examples
iex> ord = Funx.Monad.Either.lift_ord(%{
...> lt?: fn x, y -> x < y end,
...> le?: fn x, y -> x <= y end,
...> gt?: fn x, y -> x > y end,
...> ge?: fn x, y -> x >= y end
...> })
iex> ord.lt?.(Funx.Monad.Either.right(3), Funx.Monad.Either.right(5))
true
iex> ord.lt?.(Funx.Monad.Either.left(3), Funx.Monad.Either.right(5))
true
iex> ord.lt?.(Funx.Monad.Either.right(3), Funx.Monad.Either.left(5))
false
iex> ord.lt?.(Funx.Monad.Either.left(3), Funx.Monad.Either.left(5))
true
"""
@spec lift_ord(Ord.Utils.ord_t()) :: Ord.Utils.ord_map()
def lift_ord(custom_ord) do
custom_ord = Ord.Utils.to_ord_map(custom_ord)
%{
lt?: fn
%Right{right: v1}, %Right{right: v2} -> custom_ord.lt?.(v1, v2)
%Left{left: v1}, %Left{left: v2} -> custom_ord.lt?.(v1, v2)
%Left{}, %Right{} -> true
%Right{}, %Left{} -> false
end,
le?: fn
%Right{right: v1}, %Right{right: v2} -> custom_ord.le?.(v1, v2)
%Left{left: v1}, %Left{left: v2} -> custom_ord.le?.(v1, v2)
%Left{}, %Right{} -> true
%Right{}, %Left{} -> false
end,
gt?: fn
%Right{right: v1}, %Right{right: v2} -> custom_ord.gt?.(v1, v2)
%Left{left: v1}, %Left{left: v2} -> custom_ord.gt?.(v1, v2)
%Right{}, %Left{} -> true
%Left{}, %Right{} -> false
end,
ge?: fn
%Right{right: v1}, %Right{right: v2} -> custom_ord.ge?.(v1, v2)
%Left{left: v1}, %Left{left: v2} -> custom_ord.ge?.(v1, v2)
%Right{}, %Left{} -> true
%Left{}, %Right{} -> false
end
}
end
@doc """
Transforms the `Left` value using the given function if the `Either` is a `Left`.
If the value is `Right`, it is returned unchanged.
## Examples
iex> Funx.Monad.Either.map_left(Funx.Monad.Either.left("error"), fn e -> "wrapped: " <> e end)
%Funx.Monad.Either.Left{left: "wrapped: error"}
iex> Funx.Monad.Either.map_left(Funx.Monad.Either.right(42), fn _ -> "ignored" end)
%Funx.Monad.Either.Right{right: 42}
"""
@spec map_left(t(error, value), (error -> new_error)) :: t(new_error, value)
when error: term(), new_error: term(), value: term()
def map_left(%Left{left: error}, func) when is_function(func, 1), do: left(func.(error))
def map_left(%Right{} = right, _func), do: right
@doc """
Removes `Left` values from a list of `Either` and returns a list of unwrapped `Right` values.
Useful for discarding failed computations while keeping successful results.
## Examples
iex> Funx.Monad.Either.concat([Funx.Monad.Either.right(1), Funx.Monad.Either.left(:error), Funx.Monad.Either.right(2)])
[1, 2]
iex> Funx.Monad.Either.concat([Funx.Monad.Either.left(:a), Funx.Monad.Either.left(:b)])
[]
iex> Funx.Monad.Either.concat([Funx.Monad.Either.right("a"), Funx.Monad.Either.right("b"), Funx.Monad.Either.right("c")])
["a", "b", "c"]
"""
@spec concat([t(error, value)]) :: [value]
when error: term(), value: any()
def concat(list) when is_list(list) do
list
|> fold_l([], fn
%Right{right: value}, acc -> [value | acc]
%Left{}, acc -> acc
end)
|> :lists.reverse()
end
@doc """
Applies the given function to each element in the list and collects the `Right` results, discarding any `Left`.
This is useful when mapping a function that may fail and you only want the successful results.
## Examples
iex> Funx.Monad.Either.concat_map([1, 2, 3], fn x -> if rem(x, 2) == 1, do: Funx.Monad.Either.right(x), else: Funx.Monad.Either.left(:even) end)
[1, 3]
iex> Funx.Monad.Either.concat_map([2, 4], fn x -> if x > 3, do: Funx.Monad.Either.right(x), else: Funx.Monad.Either.left(:too_small) end)
[4]
iex> Funx.Monad.Either.concat_map([], fn _ -> Funx.Monad.Either.left(:none) end)
[]
"""
@spec concat_map([input], (input -> t(error, output))) :: [output]
when input: any(), output: any(), error: any()
def concat_map(list, func) when is_list(list) and is_function(func, 1) do
fold_l(list, [], fn item, acc ->
case func.(item) do
%Right{right: value} -> [value | acc]
%Left{} -> acc
end
end)
|> :lists.reverse()
end
@doc """
Sequences a list of `Either` values into an `Either` of a list.
## Examples
iex> Funx.Monad.Either.sequence([Funx.Monad.Either.right(1), Funx.Monad.Either.right(2)])
%Funx.Monad.Either.Right{right: [1, 2]}
iex> Funx.Monad.Either.sequence([Funx.Monad.Either.right(1), Funx.Monad.Either.left("error")])
%Funx.Monad.Either.Left{left: "error"}
"""
@spec sequence([t(error, value)]) :: t(error, [value]) when error: term(), value: term()
def sequence(list) when is_list(list), do: traverse(list, fn x -> x end)
@doc """
Traverses a list, applying the given function to each element and collecting the results in a single `Right`, or short-circuiting with the first `Left`.
This is useful for validating or transforming a list of values where each step may fail.
## Examples
iex> Funx.Monad.Either.traverse([1, 2, 3], &Funx.Monad.Either.right/1)
%Funx.Monad.Either.Right{right: [1, 2, 3]}
iex> Funx.Monad.Either.traverse([1, -2, 3], fn x -> if x > 0, do: Funx.Monad.Either.right(x), else: Funx.Monad.Either.left("error") end)
%Funx.Monad.Either.Left{left: "error"}
"""
@spec traverse([a], (a -> t(error, b))) :: t(error, [b])
when a: term(), b: term(), error: term()
def traverse([], _func), do: pure([])
def traverse(list, func) when is_list(list) and is_function(func, 1) do
list
|> Enum.reduce_while(pure([]), fn item, %Right{right: acc} ->
case func.(item) do
%Right{right: value} -> {:cont, pure([value | acc])}
%Left{} = left -> {:halt, left}
end
end)
|> map(&:lists.reverse/1)
end
@doc """
Sequences a list of `Either` values, collecting all errors from `Left` values, rather than short-circuiting.
## Examples
iex> Funx.Monad.Either.sequence_a([Funx.Monad.Either.right(1), Funx.Monad.Either.left("error"), Funx.Monad.Either.left("another error")])
%Funx.Monad.Either.Left{left: ["error", "another error"]}
"""
@spec sequence_a([t(error, value)]) :: t([error], [value])
when error: term(), value: term()
def sequence_a(list) when is_list(list), do: traverse_a(list, fn x -> x end)
@doc """
Traverses a list, applying the given function to each element and collecting the results in a single `Right`.
Unlike `traverse/2`, this version accumulates all `Left` values rather than stopping at the first failure.
It is useful for validations where you want to gather all errors at once.
## Examples
iex> validate = fn x -> Funx.Monad.Either.lift_predicate(x, &(&1 > 0), fn v -> "must be positive: \#{v}" end) end
iex> Funx.Monad.Either.traverse_a([1, 2, 3], validate)
%Funx.Monad.Either.Right{right: [1, 2, 3]}
iex> Funx.Monad.Either.traverse_a([1, -2, -3], validate)
%Funx.Monad.Either.Left{left: ["must be positive: -2", "must be positive: -3"]}
"""
@spec traverse_a([a], (a -> t([e], b))) :: t([e], [b])
when a: term(), b: term(), e: term()
def traverse_a([], _func), do: right([])
def traverse_a(list, func) when is_list(list) and is_function(func, 1) do
fold_l(list, right([]), fn item, acc_result ->
case {func.(item), acc_result} do
{%Right{right: value}, %Right{right: acc}} ->
right([value | acc])
{%Left{left: new}, %Left{left: existing}} ->
left(append(existing, coerce(new)))
{%Right{}, %Left{left: existing}} ->
left(existing)
{%Left{left: err}, %Right{}} ->
left(coerce(err))
end
end)
|> map(&:lists.reverse/1)
end
@doc """
Traverses a list, applying the given function to each element, and collects the successful `Just` results into a single `Right`.
The given function must return an `Either` of `Maybe`. `Right(Just x)` values are kept; `Right(Nothing)` values are filtered out.
If any application returns `Left`, all `Left` values are accumulated.
This is useful for effectful filtering, where you want to validate or transform elements and conditionally keep them, while still reporting all errors.
## Examples
iex> filter_positive = fn x ->
...> Funx.Monad.Either.lift_predicate(x, &is_integer/1, fn v -> "not an integer: \#{inspect(v)}" end)
...> |> Funx.Monad.map(fn x -> if x > 0, do: Funx.Monad.Maybe.just(x), else: Funx.Monad.Maybe.nothing() end)
...> end
iex> Funx.Monad.Either.wither_a([1, -2, 3], filter_positive)
%Funx.Monad.Either.Right{right: [1, 3]}
iex> Funx.Monad.Either.wither_a(["oops", -2], filter_positive)
%Funx.Monad.Either.Left{left: ["not an integer: \\"oops\\""]}
"""
@spec wither_a([a], (a -> t([e], Maybe.t(b)))) :: t([e], [b])
when a: term(), b: term(), e: term()
def wither_a([], _func), do: right([])
def wither_a(list, func) when is_list(list) and is_function(func, 1) do
fold_l(list, right([]), fn item, acc_result ->
case {func.(item), acc_result} do
{%Right{right: %Just{value: value}}, %Right{right: acc}} ->
right([value | acc])
{%Right{right: %Nothing{}}, %Right{right: acc}} ->
right(acc)
{%Left{left: new}, %Left{left: existing}} ->
left(append(existing, coerce(new)))
{%Right{}, %Left{left: existing}} ->
left(existing)
{%Left{left: err}, %Right{}} ->
left(coerce(err))
end
end)
|> map(&:lists.reverse/1)
end
@doc """
Validates a value using a list of validator functions. Each validator returns an `Either.Right` if
the check passes, or an `Either.Left` with an error message if it fails. If any validation fails,
all errors are aggregated and returned in a single `Left`.
### Flat list aggregation
When using the default aggregation strategy, errors are collected in a plain list:
```elixir
validate_positive = fn x ->
Funx.Monad.Either.lift_predicate(x, &(&1 > 0), fn v -> "Value must be positive: " <> to_string(v) end)
end
validate_even = fn x ->
Funx.Monad.Either.lift_predicate(x, &(rem(&1, 2) == 0), fn v -> "Value must be even: " <> to_string(v) end)
end
Funx.Monad.Either.validate(4, [validate_positive, validate_even])
#=> %Funx.Monad.Either.Right{right: 4}
Funx.Monad.Either.validate(3, [validate_positive, validate_even])
#=> %Funx.Monad.Either.Left{left: ["Value must be even: 3"]}
Funx.Monad.Either.validate(-3, [validate_positive, validate_even])
#=> %Funx.Monad.Either.Left{left: ["Value must be positive: -3", "Value must be even: -3"]}
```
### Structured aggregation with `ValidationError`
You can also use a custom struct to hold errors. This example uses `ValidationError` and a corresponding
`Funx.Semigroup` implementation to accumulate errors into a single structure:
```elixir
alias Funx.Errors.ValidationError
validate_positive = fn x ->
Funx.Monad.Either.lift_predicate(x, &(&1 > 0), fn v -> "Value must be positive: " <> to_string(v) end)
|> Funx.Monad.Either.map_left(&ValidationError.new/1)
end
validate_even = fn x ->
Funx.Monad.Either.lift_predicate(x, &(rem(&1, 2) == 0), fn v -> "Value must be even: " <> to_string(v) end)
|> Funx.Monad.Either.map_left(&ValidationError.new/1)
end
Funx.Monad.Either.validate(-3, [validate_positive, validate_even])
#=> %Funx.Monad.Either.Left{
# left: %ValidationError{
# errors: ["Value must be positive: -3", "Value must be even: -3"]
# }
# }
```
"""
@spec validate(value, [(value -> t(error, any))]) :: t([error], value)
when error: term(), value: term()
def validate(value, validators) when is_list(validators) do
traverse_a(validators, fn validator -> validator.(value) end)
|> map(fn _ -> value end)
end
def validate(value, validator) when is_function(validator, 1) do
validate(value, [validator])
end
@doc """
Converts a `Maybe` value to an `Either`. If the `Maybe` is `Nothing`, a `Left` is returned using `on_none`.
## Examples
iex> Funx.Monad.Either.lift_maybe(Funx.Monad.Maybe.just(5), fn -> "error" end)
%Funx.Monad.Either.Right{right: 5}
iex> Funx.Monad.Either.lift_maybe(Funx.Monad.Maybe.nothing(), fn -> "error" end)
%Funx.Monad.Either.Left{left: "error"}
"""
@spec lift_maybe(Maybe.t(any()), (-> any())) :: t(any(), any())
def lift_maybe(maybe, on_none) when is_struct(maybe, Just) or is_struct(maybe, Nothing) do
maybe
|> fold_l(
fn value -> right(value) end,
fn -> left(on_none.()) end
)
end
@doc """
Lifts a value into an `Either` based on the result of a predicate.
Returns `Right(value)` if the predicate returns `true`, or `Left(on_false.(value))` if it returns `false`.
This allows you to wrap a conditional check in a functional context with a custom error message.
## Examples
iex> Funx.Monad.Either.lift_predicate(5, fn x -> x > 3 end, fn x -> "\#{x} is too small" end)
%Funx.Monad.Either.Right{right: 5}
iex> Funx.Monad.Either.lift_predicate(2, fn x -> x > 3 end, fn x -> "\#{x} is too small" end)
%Funx.Monad.Either.Left{left: "2 is too small"}
"""
@spec lift_predicate(value, (value -> boolean), (value -> error)) :: t(error, value)
when value: term(), error: term()
def lift_predicate(value, predicate, on_false)
when is_function(predicate, 1) and is_function(on_false, 1) do
fold_l(
fn -> predicate.(value) end,
fn -> right(value) end,
fn -> left(on_false.(value)) end
)
end
@doc """
Converts a result (`{:ok, _}` or `{:error, _}`) to an `Either`.
## Examples
iex> Funx.Monad.Either.from_result({:ok, 5})
%Funx.Monad.Either.Right{right: 5}
iex> Funx.Monad.Either.from_result({:error, "error"})
%Funx.Monad.Either.Left{left: "error"}
"""
@spec from_result({:ok, right} | {:error, left}) :: t(left, right)
when left: term(), right: term()
def from_result({:ok, value}), do: right(value)
def from_result({:error, reason}), do: left(reason)
@doc """
Converts an `Either` to a result (`{:ok, value}` or `{:error, reason}`).
## Examples
iex> Funx.Monad.Either.to_result(Funx.Monad.Either.right(5))
{:ok, 5}
iex> Funx.Monad.Either.to_result(Funx.Monad.Either.left("error"))
{:error, "error"}
"""
@spec to_result(t(left, right)) :: {:ok, right} | {:error, left}
when left: term(), right: term()
def to_result(either) when is_struct(either, Right) or is_struct(either, Left) do
case either do
%Right{right: value} -> {:ok, value}
%Left{left: reason} -> {:error, reason}
end
end
@doc """
Wraps a value in an `Either`, catching any exceptions. If an exception occurs, a `Left` is returned with the exception.
## Examples
iex> Funx.Monad.Either.from_try(fn -> 5 end)
%Funx.Monad.Either.Right{right: 5}
iex> Funx.Monad.Either.from_try(fn -> raise "error" end)
%Funx.Monad.Either.Left{left: %RuntimeError{message: "error"}}
"""
@spec from_try((-> right)) :: t(Exception.t(), right) when right: term()
def from_try(func) do
try do
result = func.()
right(result)
rescue
exception ->
left(exception)
end
end
@doc """
Converts an `Either` to its inner value, raising an exception if it is `Left`.
If the `Left` holds an exception struct, it is raised directly. If it holds a string or list of errors, they are converted into a `RuntimeError`. Unexpected types are inspected and raised as a `RuntimeError`.
## Examples
iex> Funx.Monad.Either.to_try!(Funx.Monad.Either.right(5))
5
iex> Funx.Monad.Either.to_try!(Funx.Monad.Either.left("error"))
** (RuntimeError) error
iex> Funx.Monad.Either.to_try!(Funx.Monad.Either.left(["error 1", "error 2"]))
** (RuntimeError) error 1, error 2
iex> Funx.Monad.Either.to_try!(Funx.Monad.Either.left(%ArgumentError{message: "bad argument"}))
** (ArgumentError) bad argument
"""
@spec to_try!(t(left, right)) :: right | no_return
when left: term(), right: term()
def to_try!(%Right{right: value}), do: value
def to_try!(%Left{left: reason}) do
raise normalize_reason(reason)
end
defp normalize_reason(%_{} = exception), do: exception
defp normalize_reason(reason) when is_list(reason),
do: Enum.map_join(reason, ", ", &to_string/1)
defp normalize_reason(reason) when is_binary(reason), do: reason
defp normalize_reason(reason), do: "Unexpected error: #{inspect(reason)}"
end