Funx.Monad.Maybe.Dsl (funx v0.8.4)

View Source

Provides the maybe/2 macro for writing declarative pipelines in the Maybe context.

The DSL lets you express a sequence of operations that may return nothing without manually threading values through bind, map, or filter. Input is lifted into Maybe automatically, each step runs in order, and the pipeline stops on the first Nothing.

Supported Operations

  • bind - for operations that return Maybe, Either, result tuples, or nil
  • map - for transformations that return plain values
  • ap - for applying a function in a Maybe to a value in a Maybe
  • Maybe functions: or_else
  • Protocol functions: tap (via Funx.Tappable), filter, filter_map, guard (via Funx.Filterable)

The result format is controlled by the :as option (:maybe, :raise, or :nil).

Short-Circuit Behavior

The DSL uses fail-fast semantics. When any step returns a Nothing value, Either.Left, {:error, _} tuple, or nil, the pipeline stops immediately and returns Nothing. Subsequent steps are never executed.

Example:

iex> defmodule GetUser do
...>   use Funx.Monad.Maybe
...>   @behaviour Funx.Monad.Behaviour.Bind
...>   def bind(_value, _opts, _env), do: nothing()
...> end
iex> defmodule CheckPermissions do
...>   use Funx.Monad.Maybe
...>   @behaviour Funx.Monad.Behaviour.Bind
...>   def bind(value, _opts, _env), do: just(value)
...> end
iex> defmodule FormatUser do
...>   @behaviour Funx.Monad.Behaviour.Map
...>   def map(value, _opts, _env), do: "formatted: #{value}"
...> end
iex> use Funx.Monad.Maybe
iex> maybe 123 do
...>   bind GetUser           # Returns Nothing
...>   bind CheckPermissions  # Never runs
...>   map FormatUser         # Never runs
...> end
%Funx.Monad.Maybe.Nothing{}

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.Maybe
...>   @behaviour Funx.Monad.Behaviour.Bind
...>   def bind(value, _opts, _env) when is_binary(value) do
...>     case Integer.parse(value) do
...>       {int, ""} -> just(int)
...>       _ -> nothing()
...>     end
...>   end
...> end
iex> defmodule Double do
...>   @behaviour Funx.Monad.Behaviour.Map
...>   def map(value, _opts, _env), do: value * 2
...> end
iex> use Funx.Monad.Maybe
iex> maybe "42" do
...>   bind ParseInt
...>   map Double
...> end
%Funx.Monad.Maybe.Just{value: 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:

maybe user_id, transformers: [MyCustomTransformer] do
  bind GetUser
  map Transform
end

Transformers run at compile time and create compile-time dependencies.

Example

maybe user_id, as: :nil 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 filter operations:

filter &Validator.positive?/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

maybe(input, list)

(macro)

maybe(input, opts, list)

(macro)