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 successful Right effect.
  • left/1 – Wraps a value in a failing Left effect.
  • pure/1 – Alias for right/1.

Execution

  • run/2 – Executes the deferred effect and returns an Either result (Right or Left).

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 first Left.
  • traverse/2 – Applies a function returning an Effect to each element of a list, sequencing results.
  • sequence_a/2 – Runs a list of effects, collecting all Left errors instead of short-circuiting.
  • traverse_a/3 – Like traverse/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 a Left using a function, leaving Right values unchanged.
  • flip_either/1 – Inverts the success and failure branches of an Effect.

Lifting

  • lift_func/2 – Lifts a thunk that returns any value into an Effect, wrapping it in Right. If the thunk raises, the error is captured as a Left(EffectError).
  • lift_either/2 – Lifts a thunk that returns an Either into an Effect. Evaluation is deferred until the effect is run. Errors are also captured and wrapped in Left(EffectError).
  • lift_maybe/3 – Lifts a Maybe into an Effect, using a fallback error if the value is Nothing.
  • lift_predicate/3 – Lifts a predicate check into an Effect. Returns Right(value) if the predicate passes; otherwise returns Left(fallback).

Reader Operations

  • ask/0 – Returns the environment passed to run/2 as a Right.
  • asks/1 – Applies a function to the environment passed to run/2, wrapping the result in a Right.
  • fail/0 – Returns the environment passed to run/2 as a Left.
  • fails/1 – Applies a function to the environment passed to run/2, wrapping the result in a Left.

Elixir Interop

  • from_result/2 – Converts a {:ok, _} or {:error, _} tuple into an Effect.
  • to_result/1 – Converts an Effect to {:ok, _} or {:error, _}.
  • from_try/2 – Executes a function, catching exceptions into a Left.
  • to_try!/1 – Extracts the value from a Right, or raises an exception if Left.

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 to run/2.
  • :result – a summarized version of the result using Funx.Summarizable.
  • :effect_type:right or :left, depending on the effect being run.
  • :status:ok if the result is a Right, or :error if it's a Left.
  • :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.

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

await(task, timeout \\ 5000)

View Source
@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}}}
@spec flip_either(t(error, value)) :: t(value, error)
when error: term(), value: term()

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

from_result(result, opts \\ [])

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

from_try(func, opts_or_context \\ [])

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

left(value, opts_or_context \\ [])

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

lift_either(thunk, opts \\ [])

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

lift_func(thunk, opts \\ [])

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

lift_maybe(maybe, on_none, opts \\ [])

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

lift_predicate(value, predicate, on_false, opts \\ [])

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

pure(value, opts_or_context \\ [])

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

right(value, opts_or_context \\ [])

View Source
@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()
Link to this function

run(effect, env, opts \\ [])

View Source
@spec run(t(left, right), map(), keyword()) :: Funx.Monad.Either.t(left, right)
when left: term(), right: term()
Link to this function

sequence(list, opts \\ [])

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

sequence_a(list, opts \\ [])

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

to_result(effect, opts \\ [])

View Source
@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.

Link to this function

to_try!(effect, opts \\ [])

View Source
@spec to_try!(
  t(left, right),
  keyword()
) :: right | no_return()
when left: term(), right: term()

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

traverse(list, func, opts)

View Source
@spec traverse(
  [input],
  (input -> t(left, right)),
  Funx.Monad.Effect.Context.opts_or_context()
) ::
  t(left, [right])
when input: term(), left: term(), right: term()

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

traverse_a(list, func, opts)

View Source
@spec traverse_a(
  [input],
  (input -> t(error, value)),
  Funx.Monad.Effect.Context.opts_or_context()
) ::
  t([error], [value])
when input: term(), error: term(), value: term()
Link to this function

validate(value, validator, opts \\ [])

View Source
@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"]}