View Source Funx.Monad.Effect (funx v0.1.0)
The Funx.Monad.Effect
module defines the Effect
monad, which represents asynchronous computations
that may succeed (Right
) or fail (Left
). Execution is deferred until explicitly run, making
Effect
useful for structuring lazy, asynchronous workflows.
This module integrates tracing and telemetry, making it suitable for observability in concurrent
Elixir systems. All effects carry a Effect.Context
, which links operations and records spans
when run/2
is called.
Constructors
right/1
– Wraps a value in a successfulRight
effect.left/1
– Wraps a value in a failingLeft
effect.pure/1
– Alias forright/1
.
Execution
run/2
– Executes the deferred effect and returns anEither
result (Right
orLeft
).
You may pass :task_supervisor
in the opts
to run the effect under a specific Task.Supervisor
. This supervises the top-level task, any internal tasks spawned within the effect function are not supervised.
Sequencing
sequence/1
– Runs a list of effects, stopping at the firstLeft
.traverse/2
– Applies a function returning anEffect
to each element of a list, sequencing results.sequence_a/2
– Runs a list of effects, collecting allLeft
errors instead of short-circuiting.traverse_a/3
– Liketraverse/2
, but accumulates errors across the list.
Validation
validate/2
– Validates a value using one or more effectful validators.
Error Handling
map_left/2
– Transforms aLeft
using a function, leavingRight
values unchanged.flip_either/1
– Inverts the success and failure branches of anEffect
.
Lifting
lift_func/2
– Lifts a thunk that returns any value into anEffect
, wrapping it inRight
. If the thunk raises, the error is captured as aLeft(EffectError)
.lift_either/2
– Lifts a thunk that returns anEither
into anEffect
. Evaluation is deferred until the effect is run. Errors are also captured and wrapped inLeft(EffectError)
.lift_maybe/3
– Lifts aMaybe
into anEffect
, using a fallback error if the value isNothing
.lift_predicate/3
– Lifts a predicate check into anEffect
. ReturnsRight(value)
if the predicate passes; otherwise returnsLeft(fallback)
.
Reader Operations
ask/0
– Returns the environment passed torun/2
as aRight
.asks/1
– Applies a function to the environment passed torun/2
, wrapping the result in aRight
.fail/0
– Returns the environment passed torun/2
as aLeft
.fails/1
– Applies a function to the environment passed torun/2
, wrapping the result in aLeft
.
Elixir Interop
from_result/2
– Converts a{:ok, _}
or{:error, _}
tuple into anEffect
.to_result/1
– Converts anEffect
to{:ok, _}
or{:error, _}
.from_try/2
– Executes a function, catching exceptions into aLeft
.to_try!/1
– Extracts the value from aRight
, or raises an exception ifLeft
.
Protocols
The Left and Right structs implement the following protocols:
- Funx.Monad – Provides map/2, ap/2, and bind/2 for compositional workflows.
Although protocol implementations are defined on Left and Right individually, the behavior is unified under the Effect abstraction.
This module enables structured concurrency, error handling, and observability in asynchronous workflows.
Telemetry
The run/2
function emits telemetry using :telemetry.span/3
.
Events
[:funx, :effect, :run, :start]
[:funx, :effect, :run, :stop]
Measurements
:monotonic_time
– included in both:start
and:stop
events.:system_time
– included only in the:start
event.:duration
– included only in the:stop
event.
Metadata
:timeout
– the timeout in milliseconds passed torun/2
.:result
– a summarized version of the result usingFunx.Summarizable
.:effect_type
–:right
or:left
, depending on the effect being run.:status
–:ok
if the result is aRight
, or:error
if it's aLeft
.:trace_id
– optional value used to correlate traces across boundaries.:span_name
– optional name for the span (defaults to"funx.effect.run"
).:telemetry_span_context
– reference to correlate:start
and:stop
events.
Example
:telemetry.attach(
"effect-run-handler",
[:funx, :effect, :run, :stop],
fn event, measurements, metadata, _config ->
IO.inspect({event, measurements, metadata}, label: "Effect telemetry")
end,
nil
)
Summary
Types
Represents a deferred computation in the Effect
monad that may either succeed (Right
) or fail (Left
).
Functions
Returns a Funx.Monad.Effect.Right
that yields the environment passed to Funx.Monad.Effect.run/2
.
Returns a Funx.Monad.Effect.Right
that applies the given function to the environment passed to Funx.Monad.Effect.run/2
.
Returns a Funx.Monad.Effect.Left
that fails with the entire environment passed to Funx.Monad.Effect.run/2
.
Returns a Funx.Monad.Effect.Left
that applies the given function to the environment passed to Funx.Monad.Effect.run/2
.
Inverts the success and failure branches of an Effect
.
Converts an Elixir {:ok, value}
or {:error, reason}
tuple into an Effect
.
Wraps a function in an Effect
, catching exceptions and wrapping them in a Left
.
Wraps a value in the Left
variant of the Effect
monad, representing a failed asynchronous computation.
Lifts a thunk that returns an Either
into the Effect
monad.
Lifts a thunk into the Effect
monad, wrapping its result in a Right
.
Converts a Maybe
value into the Effect
monad.
If the Maybe
is Just
, the value is wrapped in Right
.
If it is Nothing
, the result of on_none
is wrapped in Left
.
Lifts a value into the Effect
monad based on a predicate.
If the predicate returns true, the value is wrapped in Right
.
Otherwise, the result of calling on_false
with the value is wrapped in Left
.
Transforms the Left
branch of an Effect
.
Alias for right/2
.
Wraps a value in the Right
variant of the Effect
monad, representing a successful asynchronous computation.
Runs the Effect
and returns the result, awaiting the task if necessary.
Sequences a list of Effect
computations, running each in order.
Sequences a list of Effect
computations, collecting all Right
results
or accumulating all Left
errors if present.
Converts an Effect
into an Elixir {:ok, _}
or {:error, _}
tuple by running the effect.
Executes an Effect
and returns the result if it is a Right
. If the result is a Left
,
this function raises the contained error.
Traverses a list with a function that returns Effect
computations,
running each in sequence and collecting the Right
results.
Traverses a list with a function that returns Effect
values, combining results
into a single Effect
. Unlike traverse/2
, this version accumulates all errors
rather than stopping at the first Left
.
Validates a value using one or more validator functions, each returning an Effect
.
Types
@type t(left, right) :: Funx.Monad.Effect.Left.t(left) | Funx.Monad.Effect.Right.t(right)
Represents a deferred computation in the Effect
monad that may either succeed (Right
) or fail (Left
).
This type unifies Effect.Right.t/1
and Effect.Left.t/1
under a common interface, allowing code to
operate over asynchronous effects regardless of success or failure outcome.
Each variant carries a context
for telemetry and a deferred effect
function that takes an environment.
Functions
@spec ask() :: Funx.Monad.Effect.Right.t()
Returns a Funx.Monad.Effect.Right
that yields the environment passed to Funx.Monad.Effect.run/2
.
This is the Reader-style ask
, used to access the full environment inside an effectful computation.
Example
iex> Funx.Monad.Effect.ask()
...> |> Funx.Monad.map(& &1[:region])
...> |> Funx.Monad.Effect.run(%{region: "us-west"})
%Funx.Monad.Either.Right{right: "us-west"}
@spec asks((term() -> term())) :: Funx.Monad.Effect.Right.t()
Returns a Funx.Monad.Effect.Right
that applies the given function to the environment passed to Funx.Monad.Effect.run/2
.
This allows extracting a value from the environment and using it in an effectful computation, following the Reader pattern.
Example
iex> Funx.Monad.Effect.asks(fn env -> env[:user] end)
...> |> Funx.Monad.bind(fn user -> Funx.Monad.Effect.right(user) end)
...> |> Funx.Monad.Effect.run(%{user: "alice"})
%Funx.Monad.Either.Right{right: "alice"}
@spec await(Task.t(), timeout()) :: Funx.Monad.Either.t(any(), any())
@spec fail() :: Funx.Monad.Effect.Left.t()
Returns a Funx.Monad.Effect.Left
that fails with the entire environment passed to Funx.Monad.Effect.run/2
.
This is the Reader-style equivalent of ask/0
, but marks the environment as a failure.
Useful when the presence of certain runtime data should short-circuit execution.
Example
iex> Funx.Monad.Effect.fail()
...> |> Funx.Monad.Effect.run(%{error: :invalid_token})
%Funx.Monad.Either.Left{left: %{error: :invalid_token}}
@spec fails((term() -> term())) :: Funx.Monad.Effect.Left.t()
Returns a Funx.Monad.Effect.Left
that applies the given function to the environment passed to Funx.Monad.Effect.run/2
.
This is the failure-side equivalent of asks/1
, used to produce an error effect based on runtime context.
Example
iex> Funx.Monad.Effect.fails(fn env -> {:missing_key, env} end)
...> |> Funx.Monad.Effect.run(%{input: nil})
%Funx.Monad.Either.Left{left: {:missing_key, %{input: nil}}}
Inverts the success and failure branches of an Effect
.
For a Right
, this reverses the result: a successful value becomes a failure, and
a failure becomes a success. For a Left
, only failure is expected; if the Left
produces a success, it is ignored.
This is useful when you want to reverse the semantics of a computation—treating an expected error as success, or vice versa.
Examples
iex> effect = Funx.Monad.Effect.pure(42)
iex> flipped = Funx.Monad.Effect.flip_either(effect)
iex> Funx.Monad.Effect.run(flipped)
%Funx.Monad.Either.Left{left: 42}
iex> effect = Funx.Monad.Effect.left("fail")
iex> flipped = Funx.Monad.Effect.flip_either(effect)
iex> Funx.Monad.Effect.run(flipped)
%Funx.Monad.Either.Right{right: "fail"}
@spec from_result( {:ok, right} | {:error, left}, Funx.Monad.Effect.Context.opts_or_context() ) :: t(left, right) when left: term(), right: term()
Converts an Elixir {:ok, value}
or {:error, reason}
tuple into an Effect
.
Accepts an optional context context which includes telemetry tracking.
Examples
iex> result = Funx.Monad.Effect.from_result({:ok, 42})
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Right{right: 42}
iex> result = Funx.Monad.Effect.from_result({:error, "error"})
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Left{left: "error"}
@spec from_try((-> right), Funx.Monad.Effect.Context.opts_or_context()) :: t(Exception.t(), right) when right: term()
Wraps a function in an Effect
, catching exceptions and wrapping them in a Left
.
You can optionally provide a Effect.Context
for telemetry and span propagation.
Examples
iex> result = Funx.Monad.Effect.from_try(fn -> 42 end)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Right{right: 42}
iex> result = Funx.Monad.Effect.from_try(fn -> raise "error" end)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Left{left: %RuntimeError{message: "error"}}
@spec left(left, Funx.Monad.Effect.Context.opts_or_context()) :: t(left, term()) when left: term()
Wraps a value in the Left
variant of the Effect
monad, representing a failed asynchronous computation.
Accepts either a keyword list of context options or a Effect.Context
struct.
Examples
iex> result = Funx.Monad.Effect.left("error")
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Left{left: "error"}
iex> context = Funx.Monad.Effect.Context.new(trace_id: "err-id", span_name: "failure")
iex> result = Funx.Monad.Effect.left("error", context)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Left{left: "error"}
@spec lift_either( (-> Funx.Monad.Either.t(left, right)), Funx.Monad.Effect.Context.opts_or_context() ) :: t(left, right) when left: term(), right: term()
Lifts a thunk that returns an Either
into the Effect
monad.
Instead of passing an Either
value directly, you provide a zero-arity function (thunk
) that returns one.
This defers execution until the effect is run, allowing integration with tracing and composable pipelines.
You may also pass a context or options (opts
) to configure telemetry or span metadata.
If the thunk raises an exception, it is caught and returned as a Left
containing an EffectError
tagged with :lift
.
Examples
iex> result = Funx.Monad.Effect.lift_either(fn -> %Funx.Monad.Either.Right{right: 42} end)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Right{right: 42}
iex> result = Funx.Monad.Effect.lift_either(fn -> %Funx.Monad.Either.Left{left: "error"} end)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Left{left: "error"}
@spec lift_func((-> right), Funx.Monad.Effect.Context.opts_or_context()) :: t(left, right) when left: term(), right: term()
Lifts a thunk into the Effect
monad, wrapping its result in a Right
.
This function defers execution of the given zero-arity function (thunk
) until the effect is run.
The result is automatically wrapped as Either.Right
.
You may also pass a context or options (opts
) to configure telemetry or span metadata.
If the thunk raises an exception, it is caught and returned as a Left
containing an EffectError
tagged with :lift
.
Examples
iex> result = Funx.Monad.Effect.lift_func(fn -> 42 end)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Right{right: 42}
iex> result = Funx.Monad.Effect.lift_func(fn -> raise "boom" end)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Left{
left: %Funx.Errors.EffectError{stage: :lift_func, reason: %RuntimeError{message: "boom"}}
}
@spec lift_maybe( Funx.Monad.Maybe.t(right), (-> left), Funx.Monad.Effect.Context.opts_or_context() ) :: t(left, right) when left: term(), right: term()
Converts a Maybe
value into the Effect
monad.
If the Maybe
is Just
, the value is wrapped in Right
.
If it is Nothing
, the result of on_none
is wrapped in Left
.
You can optionally provide context metadata via opts
.
Examples
iex> maybe = Funx.Monad.Maybe.just(42)
iex> result = Funx.Monad.Effect.lift_maybe(maybe, fn -> "No value" end)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Right{right: 42}
iex> maybe = Funx.Monad.Maybe.nothing()
iex> result = Funx.Monad.Effect.lift_maybe(maybe, fn -> "No value" end)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Left{left: "No value"}
@spec lift_predicate( term(), (term() -> boolean()), (term() -> left), Funx.Monad.Effect.Context.opts_or_context() ) :: t(left, term()) when left: term()
Lifts a value into the Effect
monad based on a predicate.
If the predicate returns true, the value is wrapped in Right
.
Otherwise, the result of calling on_false
with the value is wrapped in Left
.
Optional context metadata (e.g. :span_name
, :trace_id
) can be passed via opts
.
Examples
iex> result = Funx.Monad.Effect.lift_predicate(10, &(&1 > 5), fn x -> "#{x} is too small" end)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Right{right: 10}
iex> result = Funx.Monad.Effect.lift_predicate(3, &(&1 > 5), fn x -> "#{x} is too small" end)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Left{left: "3 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
branch of an Effect
.
If the Effect
resolves to a Left
, the provided function is applied to the error.
If the Effect
resolves to a Right
, the value is returned unchanged.
This function is useful when you want to rewrite or wrap errors without affecting successful computations.
Examples
iex> effect = Funx.Monad.Effect.left("error")
iex> transformed = Funx.Monad.Effect.map_left(effect, fn e -> "wrapped: " <> e end)
iex> Funx.Monad.Effect.run(transformed)
%Funx.Monad.Either.Left{left: "wrapped: error"}
iex> effect = Funx.Monad.Effect.pure(42)
iex> transformed = Funx.Monad.Effect.map_left(effect, fn _ -> "should not be called" end)
iex> Funx.Monad.Effect.run(transformed)
%Funx.Monad.Either.Right{right: 42}
@spec pure(right, Funx.Monad.Effect.Context.opts_or_context()) :: t(term(), right) when right: term()
Alias for right/2
.
Wraps a value in the Right
variant of the Effect
monad, representing a successful asynchronous computation.
Accepts either a keyword list of context options or a Effect.Context
struct.
Examples
iex> result = Funx.Monad.Effect.pure(42)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Right{right: 42}
iex> context = Funx.Monad.Effect.Context.new(trace_id: "custom-id", span_name: "pure example")
iex> result = Funx.Monad.Effect.pure(42, context)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Right{right: 42}
@spec right(right, Funx.Monad.Effect.Context.opts_or_context()) :: t(term(), right) when right: term()
Wraps a value in the Right
variant of the Effect
monad, representing a successful asynchronous computation.
This is an alias for pure/2
. You may optionally provide execution context, either as a keyword list or
a %Funx.Monad.Effect.Context{}
struct. The context is attached to the effect and propagated during execution.
Examples
iex> result = Funx.Monad.Effect.right(42)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Right{right: 42}
iex> context = Funx.Monad.Effect.Context.new(trace_id: "custom-id", span_name: "from right")
iex> result = Funx.Monad.Effect.right(42, context)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Right{right: 42}
@spec run(t(left, right)) :: Funx.Monad.Either.t(left, right) when left: term(), right: term()
Runs the Effect
and returns the result, awaiting the task if necessary.
You may provide optional telemetry metadata using opts
, such as :span_name
to promote the current context with a new label.
Options
:span_name
– (optional) promotes the trace to a new span with the given name.
Examples
iex> result = Funx.Monad.Effect.right(42)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Right{right: 42}
iex> result = Funx.Monad.Effect.right(42, span_name: "initial")
iex> Funx.Monad.Effect.run(result, span_name: "promoted")
%Funx.Monad.Either.Right{right: 42}
@spec run(t(left, right), map()) :: Funx.Monad.Either.t(left, right) when left: term(), right: term()
@spec run( t(left, right), keyword() ) :: Funx.Monad.Either.t(left, right) when left: term(), right: term()
@spec sequence([t(left, right)], Funx.Monad.Effect.Context.opts_or_context()) :: t(left, [right]) when left: term(), right: term()
Sequences a list of Effect
computations, running each in order.
If all effects resolve to Right
, the result is a Right
containing a list of values.
If any effect resolves to Left
, the sequencing stops early and that Left
is returned.
Each effect is executed with its own context context, and telemetry spans are emitted for observability.
Examples
iex> effects = [Funx.Monad.Effect.right(1), Funx.Monad.Effect.right(2)]
iex> result = Funx.Monad.Effect.sequence(effects)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Right{right: [1, 2]}
iex> effects = [Funx.Monad.Effect.right(1), Funx.Monad.Effect.left("error")]
iex> result = Funx.Monad.Effect.sequence(effects)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Left{left: "error"}
@spec sequence_a([t(error, value)], Funx.Monad.Effect.Context.opts_or_context()) :: t([error], [value]) when error: term(), value: term()
Sequences a list of Effect
computations, collecting all Right
results
or accumulating all Left
errors if present.
Unlike sequence/1
, which stops at the first Left
, this version continues processing
all effects, returning a list of errors if any failures occur.
Each effect emits its own telemetry span, and error contexts are preserved through tracing.
Examples
iex> effects = [
...> Funx.Monad.Effect.right(1),
...> Funx.Monad.Effect.left("Error 1"),
...> Funx.Monad.Effect.left("Error 2")
...> ]
iex> result = Funx.Monad.Effect.sequence_a(effects)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Left{left: ["Error 1", "Error 2"]}
@spec to_result( t(left, right), keyword() ) :: {:ok, right} | {:error, left} when left: term(), right: term()
Converts an Effect
into an Elixir {:ok, _}
or {:error, _}
tuple by running the effect.
If the effect completes successfully (Right
), the result is wrapped in {:ok, value}
.
If the effect fails (Left
), the error is returned as {:error, reason}
.
This function also emits telemetry via run/2
and supports optional context metadata through keyword options.
Options
:span_name
– sets a custom span name for tracing and telemetry.
Examples
iex> effect = Funx.Monad.Effect.right(42, span_name: "convert-ok")
iex> Funx.Monad.Effect.to_result(effect, span_name: "to_result")
{:ok, 42}
iex> error = Funx.Monad.Effect.left("fail", span_name: "convert-error")
iex> Funx.Monad.Effect.to_result(error, span_name: "to_result")
{:error, "fail"}
Telemetry will include the promoted span name ("to_result -> convert-ok"
) and context metadata.
Executes an Effect
and returns the result if it is a Right
. If the result is a Left
,
this function raises the contained error.
This is useful when you want to interoperate with code that expects regular exceptions, such as within test assertions or imperative pipelines.
Runs the effect with full telemetry tracing.
Examples
iex> effect = Funx.Monad.Effect.right(42, span_name: "return")
iex> Funx.Monad.Effect.to_try!(effect)
42
iex> error = Funx.Monad.Effect.left(%RuntimeError{message: "failure"}, span_name: "error")
iex> Funx.Monad.Effect.to_try!(error)
** (RuntimeError) failure
Telemetry will emit a :stop
event with :status
set to :ok
or :error
, depending on the outcome.
Traverses a list with a function that returns Effect
computations,
running each in sequence and collecting the Right
results.
If all effects resolve to Right
, returns a single Effect
with a list of results.
If any effect resolves to Left
, the traversal stops early and returns that Left
.
Each step preserves context context and emits telemetry spans, including nested spans when bound.
Examples
iex> is_positive = fn num ->
...> Funx.Monad.Effect.lift_predicate(num, fn x -> x > 0 end, fn x -> Integer.to_string(x) <> " is not positive" end)
...> end
iex> result = Funx.Monad.Effect.traverse([1, 2, 3], fn num -> is_positive.(num) end)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Right{right: [1, 2, 3]}
iex> result = Funx.Monad.Effect.traverse([1, -2, 3], fn num -> is_positive.(num) end)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Left{left: "-2 is not positive"}
Traverses a list with a function that returns Effect
values, combining results
into a single Effect
. Unlike traverse/2
, this version accumulates all errors
rather than stopping at the first Left
.
Each successful computation contributes to the final list of results.
If any computations fail, all errors are collected and returned as a single Left
.
This function also manages telemetry trace context across all nested effects, ensuring that span relationships and trace IDs are preserved through the traversal.
Examples
iex> validate = fn n ->
...> Funx.Monad.Effect.lift_predicate(n, fn x -> x > 0 end, fn x -> Integer.to_string(x) <> " is not positive" end)
...> end
iex> result = Funx.Monad.Effect.traverse_a([1, -2, 3], validate)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Left{left: ["-2 is not positive"]}
iex> result = Funx.Monad.Effect.traverse_a([1, 2, 3], validate)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Right{right: [1, 2, 3]}
@spec validate( value, (value -> t(error, any())) | [(value -> t(error, any()))], Funx.Monad.Effect.Context.opts_or_context() ) :: t([error], value) when error: term(), value: term()
Validates a value using one or more validator functions, each returning an Effect
.
If all validators succeed (Right
), the original value is returned in a Right
.
If any validator fails (Left
), all errors are accumulated and returned as a single Left
.
This function also manages telemetry trace context across all nested validations, ensuring that span relationships and trace IDs are preserved throughout.
Supports optional opts
for span metadata (e.g. :span_name
).
Examples
iex> validate_positive = fn x ->
...> Funx.Monad.Effect.lift_predicate(x, fn n -> n > 0 end, fn n -> "Value " <> Integer.to_string(n) <> " must be positive" end)
...> end
iex> validate_even = fn x ->
...> Funx.Monad.Effect.lift_predicate(x, fn n -> rem(n, 2) == 0 end, fn n -> "Value " <> Integer.to_string(n) <> " must be even" end)
...> end
iex> validators = [validate_positive, validate_even]
iex> result = Funx.Monad.Effect.validate(4, validators)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Right{right: 4}
iex> result = Funx.Monad.Effect.validate(3, validators)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Left{left: ["Value 3 must be even"]}
iex> result = Funx.Monad.Effect.validate(-3, validators)
iex> Funx.Monad.Effect.run(result)
%Funx.Monad.Either.Left{left: ["Value -3 must be positive", "Value -3 must be even"]}