Run this notebook

Use Livebook to open this notebook and explore new ideas.

It is easy to get started, on your machine or the cloud.

Click below to open and run it in your Livebook at .

(or change your Livebook location)

# 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.
See source

Have you already installed Livebook?

If you already installed Livebook, you can configure the default Livebook location where you want to open notebooks.
Livebook up Checking status We can't reach this Livebook (but we saved your preference anyway)
Run notebook

Not yet? Install Livebook in just a minute

Livebook is open source, free, and ready to run anywhere.

Run in the cloud

on select platforms

To run on Linux, Docker, embedded devices, or Elixir’s Mix, check our README.

PLATINUM SPONSORS
SPONSORS
Code navigation with go to definition of modules and functions Read More