View Source Funx.Monad.Either (funx v0.1.0)

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

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.

Summary

Functions

Removes Left values from a list of Either and returns a list of unwrapped Right values.

Applies the given function to each element in the list and collects the Right results, discarding any Left.

Filters the value inside a Right using the given predicate. If the predicate returns false, a Left is returned using the left_func.

Swaps the Left and Right branches of the Either.

Converts a result ({:ok, _} or {:error, _}) to an Either.

Wraps a value in an Either, catching any exceptions. If an exception occurs, a Left is returned with the exception.

Retrieves the value from a Right, returning the default value if Left.

Wraps a value in the Left monad.

Returns true if the Either is a Left value.

Lifts an equality function to compare Either values

Converts a Maybe value to an Either. If the Maybe is Nothing, a Left is returned using on_none.

Creates a custom ordering function for Either values using the provided custom_ord.

Lifts a value into an Either based on the result of a predicate.

Transforms the Left value using the given function if the Either is a Left. If the value is Right, it is returned unchanged.

Returns the current Right value or invokes the fallback_fun if Left.

Alias for right/1.

Wraps a value in the Right monad.

Returns true if the Either is a Right value.

Sequences a list of Either values into an Either of a list.

Sequences a list of Either values, collecting all errors from Left values, rather than short-circuiting.

Converts an Either to a result ({:ok, value} or {:error, reason}).

Converts an Either to its inner value, raising an exception if it is Left.

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.

Traverses a list, applying the given function to each element and collecting the results in a single Right.

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.

Traverses a list, applying the given function to each element, and collects the successful Just results into a single Right.

Types

@type t(left, right) ::
  Funx.Monad.Either.Left.t(left) | Funx.Monad.Either.Right.t(right)

Functions

@spec concat([t(error, value)]) :: [value] when error: term(), value: any()

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_map([input], (input -> t(error, output))) :: [output]
when input: any(), output: any(), error: any()

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)
[]
Link to this function

filter_or_else(either, predicate, left_func)

View Source
@spec filter_or_else(t(any(), any()), (any() -> boolean()), (-> any())) ::
  t(any(), any())

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 flip(t(left, right)) :: t(right, left) when left: term(), right: term()

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 from_result({:ok, right} | {:error, left}) :: t(left, right)
when left: term(), right: term()

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_try((-> right)) :: t(Exception.t(), right) when right: term()

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"}}
Link to this function

get_or_else(either, default)

View Source
@spec get_or_else(t(any(), any()), any()) :: any()

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 left(any()) :: Funx.Monad.Either.Left.t(any())

Wraps a value in the Left monad.

Examples

iex> Funx.Monad.Either.left("error")
%Funx.Monad.Either.Left{left: "error"}
@spec left?(t(any(), any())) :: boolean()

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

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
Link to this function

lift_maybe(maybe, on_none)

View Source
@spec lift_maybe(Funx.Monad.Maybe.t(any()), (-> any())) :: t(any(), any())

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"}

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
Link to this function

lift_predicate(value, predicate, on_false)

View Source
@spec lift_predicate(value, (value -> boolean()), (value -> error)) :: t(error, value)
when value: term(), error: term()

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 map_left(t(error, value), (error -> new_error)) :: t(new_error, value)
when error: term(), new_error: term(), value: term()

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}
Link to this function

or_else(right, fallback_fun)

View Source
@spec or_else(t(error, value), (-> t(error, value))) :: t(error, value)
when error: term(), value: term()

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 pure(any()) :: Funx.Monad.Either.Right.t(any())

Alias for right/1.

Examples

iex> Funx.Monad.Either.pure(2)
%Funx.Monad.Either.Right{right: 2}
@spec right(any()) :: Funx.Monad.Either.Right.t(any())

Wraps a value in the Right monad.

Examples

iex> Funx.Monad.Either.right(5)
%Funx.Monad.Either.Right{right: 5}
@spec right?(t(any(), any())) :: boolean()

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 sequence([t(error, value)]) :: t(error, [value])
when error: term(), value: term()

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_a([t(error, value)]) :: t([error], [value])
when error: term(), value: term()

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 to_result(t(left, right)) :: {:ok, right} | {:error, left}
when left: term(), right: term()

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_try!(t(left, right)) :: right | no_return() when left: term(), right: term()

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 traverse([a], (a -> t(error, b))) :: t(error, [b])
when a: term(), b: term(), error: term()

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], (a -> t([e], b))) :: t([e], [b])
when a: term(), b: term(), e: term()

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"]}
Link to this function

validate(value, validators)

View Source
@spec validate(value, [(value -> t(error, any()))]) :: t([error], value)
when error: term(), value: term()

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:

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:

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 wither_a([a], (a -> t([e], Funx.Monad.Maybe.t(b)))) :: t([e], [b])
when a: term(), b: term(), e: term()

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\""]}