Funx.Monad.Either
Mix.install([
{:funx, "0.8.8"}
])
Overview
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 theRightbranch. -
left/1: Wraps a value in theLeftbranch. -
pure/1: Alias forright/1.
Refinement
-
right?/1: Returnstrueif the value is aRight. -
left?/1: Returnstrueif the value is aLeft.
Fallback and Extraction
-
get_or_else/2: Returns the value from aRight, or a default ifLeft. -
or_else/2: Returns the originalRight, or invokes a fallback function ifLeft. -
map_left/2: Transforms aLeftusing a function, leavingRightvalues unchanged. -
flip/1: SwapsLeftandRight, turning errors into successes and vice versa. -
filter_or_else/3: Applies a predicate to theRightvalue; if false, returns a fallbackLeft. -
tap/2: Executes a side-effect function on aRightvalue, returning the originalEitherunchanged.
List Operations
-
concat/1: Removes allLeftvalues and unwraps theRightvalues from a list. -
concat_map/2: Applies a function and collects onlyRightresults. -
sequence/1: Converts a list ofEithervalues into a singleEitherof list. -
traverse/2: Applies a function to each element in a list and sequences the results. -
sequence_a/1: Likesequence/1, but accumulates all errors fromLeftvalues. -
traverse_a/2: Liketraverse/2, but accumulates allLeftvalues instead of short-circuiting. -
wither_a/2: Liketraverse_a/2, but filters outNothingresults and collects onlyJustvalues.
Validation
-
validate/2: Applies multiple validators to a single input, collecting all errors.
Lifting
-
lift_predicate/3: Turns a predicate into anEither, returningRightontrueandLeftonfalse. -
lift_maybe/2: Converts aMaybeto anEitherusing a fallback value. -
lift_eq/1: Lifts an equality function into theEithercontext. -
lift_ord/1: Lifts an ordering function into theEithercontext.
Transformation
-
map_left/2– Transforms the error inside aLeft, leavingRightvalues untouched.
Elixir Interoperability
-
from_result/1: Converts{:ok, val}or{:error, err}into anEither. -
to_result/1: Converts anEitherinto a result tuple. -
from_try/1: Runs a function and returnsRighton success orLefton exception. -
to_try!/1: Unwraps aRight, or raises an error from aLeft.
Protocols
The Left and Right structs implement the following protocols, making the Either abstraction composable and extensible:
-
Funx.Eq: Enables equality comparisons betweenEithervalues. -
Funx.Foldable: Implementsfold_l/3andfold_r/3for reducing over contained values. -
Funx.Monad: Providesmap/2,ap/2, andbind/2for monadic composition. -
Funx.Ord: Defines ordering behavior for comparingLeftandRightvalues.
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.
Function Examples
import Funx.Monad.Either
alias Funx.Monad
alias Funx.Monad.{Either, Maybe}
alias Funx.Tappable
right/1
Wraps a value in the Right monad.
right(5)
left/1
Wraps a value in the Left monad.
left("error")
pure/1
Alias for right/1.
pure(2)
Monad.map/2
Applies a function to the value inside a Right monad. If the Either is a Left, it is returned unchanged.
This function is from the Funx.Monad module, not Funx.Monad.Either.
right(42)
|> Monad.map(&(&1 + 1))
left("error")
|> Monad.map(&(&1 + 1))
Monad.bind/2
Applies a function that returns an Either to the value inside a Right monad. This is also known as “flatMap” in other languages.
If the Either is a Left, it is returned unchanged. If the function returns a Left, that Left is returned.
This function is from the Funx.Monad module, not Funx.Monad.Either.
right(42)
|> Monad.bind(fn x -> right(div(x, 2)) end)
left("error")
|> Monad.bind(fn _ -> right(10) end)
right(42)
|> Monad.bind(fn _ -> left("computation failed") end)
Monad.ap/2
Applies a function wrapped in an Either to a value wrapped in an Either.
If either the function or the value is a Left, the Left is returned.
This function is from the Funx.Monad module, not Funx.Monad.Either.
Monad.ap(right(&(&1 + 1)), right(42))
Monad.ap(left("error"), right(42))
Monad.ap(right(&(&1 + 1)), left("error"))
left?/1
Returns true if the Either is a Left value.
left?(left("error"))
left?(right(5))
right?/1
Returns true if the Either is a Right value.
right?(right(5))
right?(left("error"))
filter_or_else/3
Filters the value inside a Right using the given predicate. If the predicate returns false,
a Left is returned using the left_func.
filter_or_else(right(5), fn x -> x > 3 end, fn -> "error" end)
filter_or_else(right(2), fn x -> x > 3 end, fn -> "error" end)
get_or_else/2
Retrieves the value from a Right, returning the default value if Left.
get_or_else(right(5), 0)
get_or_else(left("error"), 0)
or_else/2
Returns the current Right value or invokes the fallback_fun if Left.
Useful for recovering from a failure by providing an alternate computation.
or_else(left("error"), fn -> right(42) end)
or_else(right(10), fn -> right(42) end)
flip/1
Swaps the Left and Right branches of the Either.
Turns a Left into a Right and vice versa, preserving the contained term.
flip(left(:error))
flip(right(42))
lift_eq/1
Lifts an equality function to compare Either values:
-
RightvsRight: Uses the custom equality function. -
LeftvsLeft: Uses the custom equality function. -
LeftvsRightor vice versa: Alwaysfalse.
eq = lift_eq(%{
eq?: fn x, y -> x == y end,
not_eq?: fn x, y -> x != y end
})
eq.eq?.(right(5), right(5))
eq.eq?.(right(5), right(10))
eq.eq?.(left(:a), left(:a))
eq.eq?.(left(:a), left(:b))
eq.eq?.(right(5), left(:a))
lift_ord/1
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.
ord = 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
})
ord.lt?.(right(3), right(5))
ord.lt?.(left(3), right(5))
ord.lt?.(right(3), left(5))
ord.lt?.(left(3), left(5))
map_left/2
Transforms the Left value using the given function if the Either is a Left.
If the value is Right, it is returned unchanged.
map_left(left("error"), fn e -> "wrapped: " <> e end)
map_left(right(42), fn _ -> "ignored" end)
tap/2
Executes a side-effect function on a Right value and returns the original Either unchanged.
If the Either is Left, the function is not called.
Useful for debugging, logging, or performing side effects without changing the value.
# Side effect on Right
right(42)
|> Tappable.tap(&IO.inspect(&1, label: "debug"))
# No side effect on Left
left("error")
|> Tappable.tap(&IO.inspect(&1, label: "debug"))
# In a pipeline
right(5)
|> Monad.map(&(&1 * 2))
|> Tappable.tap(&IO.inspect(&1, label: "after map"))
|> Monad.map(&(&1 + 1))
concat/1
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.
concat([right(1), left(:error), right(2)])
concat([left(:a), left(:b)])
concat([right("a"), right("b"), right("c")])
concat_map/2
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.
concat_map([1, 2, 3], fn x -> if rem(x, 2) == 1, do: right(x), else: left(:even) end)
concat_map([2, 4], fn x -> if x > 3, do: right(x), else: left(:too_small) end)
concat_map([], fn _ -> left(:none) end)
sequence/1
Sequences a list of Either values into an Either of a list.
sequence([right(1), right(2)])
sequence([right(1), left("error")])
traverse/2
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.
traverse([1, 2, 3], &right/1)
traverse([1, -2, 3], fn x -> if x > 0, do: right(x), else: left("error") end)
sequence_a/1
Sequences a list of Either values, collecting all errors from Left values, rather than short-circuiting.
sequence_a([right(1), left("error"), left("another error")])
traverse_a/2
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.
validate = fn x -> lift_predicate(x, &(&1 > 0), fn v -> "must be positive: #{v}" end) end
traverse_a([1, 2, 3], validate)
traverse_a([1, -2, -3], validate)
wither_a/2
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.
filter_positive = fn x ->
lift_predicate(x, &is_integer/1, fn v -> "not an integer: #{inspect(v)}" end)
|> Funx.Monad.map(fn x -> if x > 0, do: Maybe.just(x), else: Maybe.nothing() end)
end
wither_a([1, -2, 3], filter_positive)
wither_a(["oops", -2], filter_positive)
validate/2
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 ->
lift_predicate(x, &(&1 > 0), fn v -> "Value must be positive: " <> to_string(v) end)
end
validate_even = fn x ->
lift_predicate(x, &(rem(&1, 2) == 0), fn v -> "Value must be even: " <> to_string(v) end)
end
validate(4, [validate_positive, validate_even])
validate(3, [validate_positive, validate_even])
validate(-3, [validate_positive, validate_even])
lift_maybe/2
Converts a Maybe value to an Either. If the Maybe is Nothing, a Left is returned using on_none.
lift_maybe(Maybe.just(5), fn -> "error" end)
lift_maybe(Maybe.nothing(), fn -> "error" end)
lift_predicate/3
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.
lift_predicate(5, fn x -> x > 3 end, fn x -> "#{x} is too small" end)
lift_predicate(2, fn x -> x > 3 end, fn x -> "#{x} is too small" end)
from_result/1
Converts a result ({:ok, _} or {:error, _}) to an Either.
from_result({:ok, 5})
from_result({:error, "error"})
to_result/1
Converts an Either to a result ({:ok, value} or {:error, reason}).
to_result(right(5))
to_result(left("error"))
from_try/1
Wraps a value in an Either, catching any exceptions. If an exception occurs, a Left is returned with the exception.
from_try(fn -> 5 end)
from_try(fn -> raise "error" end)
to_try!/1
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.
to_try!(right(5))
# This will raise an error:
# to_try!(left("error"))
# This will raise an error:
# to_try!(left(["error 1", "error 2"]))
# This will raise an error:
# to_try!(left(%ArgumentError{message: "bad argument"}))