# Funx.Monad.Maybe.Dsl
```elixir
Mix.install([
{:funx, path: Path.expand("../../..", __DIR__)}
])
```
## Overview
The `Funx.Monad.Maybe.Dsl` module provides a macro-based DSL for writing declarative pipelines that may return nothing. Instead of manually chaining `bind`, `map`, and other Maybe operations, you can express your logic in a clean, readable way.
The DSL automatically:
* Lifts input values into the Maybe context
* Handles missing values by short-circuiting on the first Nothing
* Provides compile-time warnings for common mistakes
* Supports multiple output formats (Maybe, nil, or raise)
## Setup
```elixir
use Funx.Monad.Maybe
```
## Basic Usage
### Understanding Maybe: Conditional Existence
`Maybe` is not about "missing data" or "errors." It's about conditional existence in domain contexts.
Consider a system that processes geometric shapes. A shape can be a circle or a rectangle. Both are valid shapes, but `radius` only exists for circles.
This isn't "a rectangle with a missing radius" - that's nonsense. A rectangle doesn't have a radius. The radius exists conditionally - only in the circle context.
### Simple Pipeline with `bind`
The `bind` operation is used when your function returns a `Maybe`, `Either`, result tuple, or `nil`.
Here are Shapes with different properties:
```elixir
defmodule Circle do
defstruct [:radius, :color]
end
defmodule Rectangle do
defstruct [:width, :height, :color]
end
defmodule Triangle do
defstruct [:base, :height, :color]
end
defmodule ShapeProcessor do
use Funx.Monad.Maybe
# Does this shape exist as a circle?
def as_circle(%Circle{} = circle), do: just(circle)
def as_circle(_other), do: nothing()
# Does this shape exist as a rectangle?
def as_rectangle(%Rectangle{} = rect), do: just(rect)
def as_rectangle(_other), do: nothing()
# Does this shape exist as a triangle?
def as_triangle(%Triangle{} = tri), do: just(tri)
def as_triangle(_other), do: nothing()
# Extract radius - only exists in circle context
def get_radius(%Circle{radius: r}), do: just(r)
def get_radius(_), do: nothing()
# Extract width - only exists in rectangle context
def get_width(%Rectangle{width: w}), do: just(w)
def get_width(_), do: nothing()
end
```
```elixir
circle = %Circle{radius: 6, color: "red"}
rectangle = %Rectangle{width: 10, height: 5, color: "blue"}
triangle = %Triangle{base: 8, height: 6, color: "green"}
```
Does this shape exist as a circle?
```elixir
maybe circle do
bind ShapeProcessor.as_circle()
bind ShapeProcessor.get_radius()
end
```
Yes!
Does this shape exist as a circle?
```elixir
maybe rectangle do
bind ShapeProcessor.as_circle()
bind ShapeProcessor.get_radius()
end
```
No, it's a rectangle.
This isn't an error - rectangles are valid, they just don't exist in the circle context
Does this shape have a width?
```elixir
maybe circle do
bind ShapeProcessor.as_rectangle()
bind ShapeProcessor.get_width()
end
```
Nope
Does this shape have a width?
```elixir
maybe rectangle do
bind ShapeProcessor.as_rectangle()
bind ShapeProcessor.get_width()
end
```
Yep, it's a rectangle.
### Using `map` for Transformations
The `map` operation transforms a value without changing the Maybe context. Use it when your function returns a plain value, not a Maybe.
Calculate area if shape is a circle:
```elixir
maybe circle do
bind ShapeProcessor.as_circle()
bind ShapeProcessor.get_radius()
map fn r -> :math.pi() * r * r end
end
```
Calculate area if shape is a rectangle:
```elixir
maybe rectangle do
bind ShapeProcessor.as_rectangle()
map fn rect -> rect.width * rect.height end
end
```
### Using tap for Side Effects
The `tap` operation executes a side-effect function on a `Just` value and returns the original `Maybe` unchanged. This is useful for debugging, logging, or performing side effects without changing the value.
If the `Maybe` is Nothing, the tap function is not called.
Side effect only runs if shape is a circle:
```elixir
maybe circle do
bind ShapeProcessor.as_circle()
tap fn c -> IO.inspect(c.radius, label: "circle radius") end
bind ShapeProcessor.get_radius()
end
```
No side effect - rectangle doesn't exist as a circle:
```elixir
maybe rectangle do
bind ShapeProcessor.as_circle()
tap fn c -> IO.inspect(c.radius, label: "should not see this") end
bind ShapeProcessor.get_radius()
end
```
## Protocol Functions
`Maybe` DSL integrates with Funx protocols for filtering and guarding based on predicates.
### Conditional Filtering
Filter based on a predicate. If the predicate returns `false`, the result becomes `Nothing`.
Get only large circles (radius > 5):
```elixir
maybe circle do
bind ShapeProcessor.as_circle()
filter fn c -> c.radius > 5 end
map fn c -> c.radius end
end
```
Small circle fails the filter:
```elixir
small_circle = %Circle{radius: 3, color: "blue"}
maybe small_circle do
bind ShapeProcessor.as_circle()
filter fn c -> c.radius > 5 end
map fn c -> c.radius end
end
```
### Predicate-Based Gates
Guard is similar to filter but uses guard syntax for clarity.
Only process red shapes:
```elixir
maybe circle do
guard fn shape -> shape.color == "red" end
bind ShapeProcessor.as_circle()
map fn c -> c.radius end
end
```
Blue circle fails the guard:
```elixir
blue_circle = %Circle{radius: 5, color: "blue"}
maybe blue_circle do
guard fn shape -> shape.color == "red" end
bind ShapeProcessor.as_circle()
map fn c -> c.radius end
end
```
## Output Formats
### Default: `Maybe`
By default, the DSL returns a `Maybe` value.
Returns `Just(radius)`:
```elixir
maybe circle do
bind ShapeProcessor.as_circle()
bind ShapeProcessor.get_radius()
end
```
Returns `Nothing` - rectangle doesn't exist as a circle:
```elixir
maybe rectangle do
bind ShapeProcessor.as_circle()
bind ShapeProcessor.get_radius()
end
```
### Nil Format
Use `as: :nil` to unwrap the value or get `nil`:
```elixir
maybe circle, as: :nil do
bind ShapeProcessor.as_circle()
bind ShapeProcessor.get_radius()
end
```
```elixir
maybe rectangle, as: :nil do
bind ShapeProcessor.as_circle()
bind ShapeProcessor.get_radius()
end
```
### Raise Format
Use `as: :raise` to unwrap the value or raise when value doesn't exist in the context:
```elixir
maybe circle, as: :raise do
bind ShapeProcessor.as_circle()
bind ShapeProcessor.get_radius()
end
```
```elixir
maybe rectangle, as: :raise do
bind ShapeProcessor.as_circle()
bind ShapeProcessor.get_radius()
end
```
### Filter and Transform
Filter and transform in one step - returns `Nothing` if the transformation returns `:error`.
Get diameter if circle and radius > 3:
```elixir
maybe circle do
bind ShapeProcessor.as_circle()
filter_map fn c ->
if c.radius > 3 do
{:ok, c.radius * 2}
else
:error
end
end
end
```
Small circle fails `filter_map`:
```elixir
small_circle = %Circle{radius: 2, color: "blue"}
maybe small_circle do
bind ShapeProcessor.as_circle()
filter_map fn c ->
if c.radius > 3 do
{:ok, c.radius * 2}
else
:error
end
end
end
```
### Providing Fallbacks
Provide a fallback when value doesn't exist in the expected context.
If not a circle, use a default radius:
```elixir
maybe rectangle do
bind ShapeProcessor.as_circle()
bind ShapeProcessor.get_radius()
or_else fn -> just(1) end
end
```
## Input Lifting
The DSL automatically lifts various input types into the `Maybe` context.
Plain values → `Just`
```elixir
maybe circle do
map fn c -> c.color end
end
```
`{:ok, value}` → `Just` (integrating with Elixir conventions):
```elixir
maybe {:ok, circle} do
bind ShapeProcessor.as_circle()
map fn c -> c.radius end
end
```
`{:error, _}` → `Nothing`:
```elixir
maybe {:error, "not found"} do
bind ShapeProcessor.as_circle()
map fn c -> c.radius end
end
```
`Maybe` values pass through unchanged:
```elixir
maybe just(circle) do
bind ShapeProcessor.as_circle()
map fn c -> c.radius end
end
```
`Nothing` short-circuits immediately:
```elixir
maybe nothing() do
bind ShapeProcessor.as_circle()
map fn c -> c.radius end
end
```
`Either` values are converted (`Right` → `Just`, `Left` → `Nothing`):
```elixir
import Funx.Monad.Either
use Funx.Monad.Either
maybe right(circle) do
bind ShapeProcessor.as_circle()
map fn c -> c.radius end
end
```
`nil` → `Nothing`:
```elixir
maybe nil do
bind ShapeProcessor.as_circle()
map fn c -> c.radius end
end
```
## Advanced: Module-Based Operations
You can create reusable operations as modules that implement specific monad behaviors. Choose the behavior based on what your operation does:
* **`Funx.Monad.Behaviour.Bind`** - for operations that can fail (used with `bind`, `tap`, `filter_map`)
* **`Funx.Monad.Behaviour.Map`** - for pure transformations (used with `map`)
* **`Funx.Monad.Behaviour.Predicate`** - for boolean tests (used with `filter`, `guard`)
* **`Funx.Monad.Behaviour.Ap`** - for applicative functors (used with `ap`)
All behaviors use the same signature: `(value, opts, env)` where:
* `value` - the current pipeline value
* `opts` - options passed with `{Module, opts}` syntax
* `env` - read-only environment for dependency injection
```elixir
defmodule ScaleShape do
@behaviour Funx.Monad.Behaviour.Map
use Funx.Monad.Maybe
@impl true
def map(shape, opts, _env) do
factor = Keyword.get(opts, :factor, 2)
case shape do
%Circle{radius: r} = c -> %{c | radius: r * factor}
%Rectangle{width: w, height: h} = r -> %{r | width: w * factor, height: h * factor}
%Triangle{base: b, height: h} = t -> %{t | base: b * factor, height: h * factor}
end
end
end
```
Use modules in the DSL with options:
```elixir
maybe circle do
bind ShapeProcessor.as_circle()
map {ScaleShape, factor: 3}
end
```
Without options, uses default behavior (2x):
```elixir
maybe circle do
bind ShapeProcessor.as_circle()
map ScaleShape
end
```
Example of a predicate module:
```elixir
defmodule IsLargeShape do
@behaviour Funx.Monad.Behaviour.Predicate
@impl true
def predicate(shape, opts, _env) do
min_size = Keyword.get(opts, :min_size, 10)
case shape do
%Circle{radius: r} -> r >= min_size
%Rectangle{width: w, height: h} -> w * h >= min_size * min_size
%Triangle{base: b, height: h} -> b * h / 2 >= min_size * min_size
_ -> false
end
end
end
```
Use with filter:
```elixir
maybe circle do
bind ShapeProcessor.as_circle()
filter {IsLargeShape, min_size: 5}
end
```
## Applicative Functor Support
The `Maybe` DSL supports applicative functors with the `ap` operation, allowing you to apply a function wrapped in a `Maybe` to a value wrapped in a `Maybe`.
Note: In the DSL, the pipeline value should be the function, and you pass the value to `ap`:
Example of ap with a simple function:
Pipeline contains the function, `ap` receives the value
```elixir
add_five = just(fn x -> x + 5 end)
maybe add_five do
ap just(10)
end
```
`ap` with `Nothing` function returns `Nothing`:
```elixir
nothing_fn = nothing()
maybe nothing_fn do
ap just(10)
end
```
`ap` on `Nothing` value returns `Nothing`:
```elixir
add_five = just(fn x -> x + 5 end)
maybe add_five do
ap nothing()
end
```
## Summary
Maybe is not just about missing data or errors. It's about conditional existence in domain contexts.
When you ask "does this value exist as X?", `Maybe` gives you:
* `Just(value)` - yes, it exists in this context
* `Nothing` - no, it doesn't exist in this context (which is not an error!)
### Core Operations
* `bind` - for operations that check domain contexts (returns Maybe, Either, result tuples, or nil)
* `map` - for transformations on values that exist in the current context
* `tap` - for side effects on values in the current context (debugging, logging)
* `ap` - for applicative functor application
### Protocol Functions
* `filter` - keep only values that match a predicate
* `filter_map` - filter and transform in one step
* `guard` - predicate-based gates
### Maybe Functions
* `or_else` - provide fallbacks when values don't exist in expected contexts
### Output Formats
* `:maybe` (default) - returns `Just(value)` or `Nothing`
* `:nil` - unwraps to value or `nil`
* `:raise` - unwraps or raises when value doesn't exist in context
### Integration
* Auto-lifting - automatic conversion of Maybe, Either, tuples, nil, and plain values
* Module support - reusable operations via specific behaviors (Bind, Map, Predicate, Ap)
* Options support - pass options to modules using `{Module, opts}` syntax
### Module Support
All these operations support modules that implement the appropriate behavior:
* **`bind Module`** - module implements Bind behavior, returns Maybe or tuple
* **`map Module`** - module implements Map behavior, returns plain value
* **`tap Module`** - module implements Bind behavior, performs side effect
* **`filter Module`** - module implements Predicate behavior, returns boolean
* **`filter_map Module`** - module implements Bind behavior, returns Maybe or tuple (like bind)
* **`guard Module`** - module implements Predicate behavior, returns boolean
Use `{Module, key: value}` syntax to pass options to any module's behavior method.