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 tuples
  • map - for transformations that return plain values
  • ap - 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: validate for 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
end

Transformers 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
end

Auto-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) becomes fn 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.

Summary

Functions

Link to this macro

either(input, list)

View Source (macro)
Link to this macro

either(input, opts, list)

View Source (macro)