View Source Funx.Monad.Either.Dsl (funx v0.3.0)
Provides the either/2 macro for writing declarative pipelines in the Either context.
The DSL lets you express a sequence of operations that may fail without manually
threading values through bind, map, or map_left. Input is lifted into Either
automatically, each step runs in order, and the pipeline stops on the first error.
Supported Operations
bind- for operations that return Either or result tuplesmap- for transformations that return plain valuesap- for applying a function in an Either to a value in an Either- Either functions:
filter_or_else,or_else,map_left,flip - Protocol functions:
tap(via Funx.Tappable) - Validation:
validatefor accumulating multiple errors
The result format is controlled by the :as option (:either, :tuple, or :raise).
Error Handling Strategy
Short-Circuit Behavior: The DSL uses fail-fast semantics. When any step returns
a Left value or {:error, reason} tuple, the pipeline stops immediately and
returns that error. Subsequent steps are never executed.
Example:
iex> defmodule GetUser do
...> use Funx.Monad.Either
...> @behaviour Funx.Monad.Either.Dsl.Behaviour
...> def run(_value, _env, _opts), do: left("not found")
...> end
iex> defmodule CheckPermissions do
...> use Funx.Monad.Either
...> @behaviour Funx.Monad.Either.Dsl.Behaviour
...> def run(value, _env, _opts), do: right(value)
...> end
iex> defmodule FormatUser do
...> @behaviour Funx.Monad.Either.Dsl.Behaviour
...> def run(value, _env, _opts), do: "formatted: #{value}"
...> end
iex> use Funx.Monad.Either
iex> either 123 do
...> bind GetUser # Returns Left("not found")
...> bind CheckPermissions # Never runs
...> map FormatUser # Never runs
...> end
%Funx.Monad.Either.Left{left: "not found"}Exception: The validate operation uses applicative semantics and accumulates
all validation errors before returning:
Example:
iex> use Funx.Monad.Either
iex> positive? = fn x -> if x > 0, do: right(x), else: left("not positive") end
iex> even? = fn x -> if rem(x, 2) == 0, do: right(x), else: left("not even") end
iex> less_than_100? = fn x -> if x < 100, do: right(x), else: left("too large") end
iex> either -5 do
...> validate [positive?, even?, less_than_100?]
...> end
%Funx.Monad.Either.Left{left: ["not positive", "not even"]}Performance
The DSL compiles to direct function calls at compile time. There is no runtime
overhead for the DSL itself - it expands into the same code you would write manually
with bind, map, etc.
Example showing compile-time expansion:
iex> defmodule ParseInt do
...> use Funx.Monad.Either
...> @behaviour Funx.Monad.Either.Dsl.Behaviour
...> def run(value, _env, _opts) when is_binary(value) do
...> case Integer.parse(value) do
...> {int, ""} -> right(int)
...> _ -> left("invalid integer")
...> end
...> end
...> end
iex> defmodule Double do
...> @behaviour Funx.Monad.Either.Dsl.Behaviour
...> def run(value, _env, _opts), do: value * 2
...> end
iex> use Funx.Monad.Either
iex> either "42" do
...> bind ParseInt
...> map Double
...> end
%Funx.Monad.Either.Right{right: 84}Auto-lifting creates anonymous functions, but these are created at compile time, not runtime. For performance-critical hot paths, you may prefer direct combinator calls, but the difference is typically negligible.
Transformers
Transformers allow post-parse optimization and validation of pipelines:
either user_id, transformers: [MyCustomTransformer] do
bind GetUser
map Transform
endTransformers run at compile time and create compile-time dependencies.
See Funx.Monad.Either.Dsl.Transformer for details on creating custom transformers.
Example
either user_id, as: :tuple do
bind Accounts.get_user()
bind Policies.ensure_active()
map fn user -> %{user: user} end
endAuto-Lifting of Function Calls
The DSL automatically lifts certain function call patterns for convenience:
Module.fun()becomes&Module.fun/1(zero-arity qualified calls)Module.fun(arg)becomesfn x -> Module.fun(x, arg) end(partial application)
This is particularly useful in validator lists:
validate [Validator.positive?(), Validator.even?()]
# Becomes: validate [&Validator.positive?/1, &Validator.even?/1]This module defines the public DSL entry point. The macro expansion details and internal rewrite rules are not part of the public API.