Macros
Monadic and applicative notation macros.
Karpal provides two macros that flatten nested monadic and applicative computations into a readable, top-to-bottom sequence of bindings. Both macros use = for binding (not <-, which is reserved in Rust edition 2024).
do_!
Monadic do-notation. Desugars sequential bindings into nested Chain::chain calls.
Syntax
do_! { F;
x = monadic_expr_1;
y = monadic_expr_2; // can reference x
// ... more bindings ...
final_monadic_expr // bare expression, no binding
}
- The first token
Fis the type constructor (OptionF,VecF,ResultF<E>, etc.), followed by a semicolon. - Each binding uses
=. Later bindings can reference names bound earlier -- the steps are sequential. - The final line is a bare expression of type
F::Of<T>. It is the value returned by the wholedo_!block. - If any step produces a short-circuiting value (
None,Err(_)), the entire block short-circuits immediately.
Expansion
Each x = expr; binding desugars into a Chain::chain call. The macro expands recursively:
// This:
do_! { F;
x = expr_a;
y = expr_b;
expr_c
}
// Expands to:
<F as Chain>::chain(expr_a, |x| {
<F as Chain>::chain(expr_b, |y| {
expr_c
})
})
A single bare expression (no bindings) is returned as-is:
do_! { F; some_expr }
// Expands to:
some_expr
Requirements
The type constructor F must implement Chain (and therefore Apply and Functor). In practice, any type that implements Monad satisfies this requirement, since Monad is a blanket trait over Applicative + Chain.
Examples
OptionF -- sequential computation with short-circuiting
use karpal_std::prelude::*;
let result = do_! { OptionF;
x = Some(1);
y = Some(x + 1); // y depends on x
OptionF::pure(x + y) // final expression wraps in Some
};
assert_eq!(result, Some(3));
OptionF -- short-circuiting on None
use karpal_std::prelude::*;
let result: Option<i32> = do_! { OptionF;
x = Some(1);
_y = None::<i32>; // short-circuits here
OptionF::pure(x) // never reached
};
assert_eq!(result, None);
OptionF -- single expression (no bindings)
use karpal_std::prelude::*;
let result = do_! { OptionF;
Some(42)
};
assert_eq!(result, Some(42));
ResultF -- chaining fallible operations
use karpal_std::prelude::*;
fn parse_port(s: &str) -> Result<u16, String> {
s.parse::<u16>().map_err(|e| e.to_string())
}
let result = do_! { ResultF<String>;
port = parse_port("8080");
validated = if port > 0 { Ok(port) } else { Err("invalid".into()) };
Ok(format!("port={}", validated))
};
assert_eq!(result, Ok("port=8080".to_string()));
VecF -- list comprehension (cartesian product)
use karpal_std::prelude::*;
let result = do_! { VecF;
x = vec![1, 2];
y = vec![10, 20];
VecF::pure(x + y)
};
assert_eq!(result, vec![11, 21, 12, 22]);
ado_!
Applicative do-notation. Collects independent bindings and combines them with Apply::ap and Functor::fmap.
Syntax
ado_! { F;
x = applicative_expr_1;
y = applicative_expr_2;
// ... up to 4 bindings ...
yield combining_expression
}
- Same first-token convention as
do_!: the type constructor, then a semicolon. - Each binding uses
=. Bindings are independent and must not reference each other. - The
yieldkeyword introduces the combining expression. This expression is a pure function of the bound names -- it is automatically lifted into the applicative context. - Supports 1 to 4 bindings.
- If any binding evaluates to a short-circuiting value (
None,Err(_)), the whole block short-circuits.
Expansion
The expansion depends on the number of bindings. With one binding, the macro uses Functor::fmap. With two or more, it builds a curried closure and applies it with Apply::ap:
1 binding
// This:
ado_! { F; x = expr; yield body }
// Expands to:
<F as Functor>::fmap(expr, |x| body)
2 bindings
// This:
ado_! { F; x = e1; y = e2; yield body }
// Expands to:
<F as Apply>::ap(
<F as Functor>::fmap(e1, |x| move |y| body),
e2,
)
3 bindings
// This:
ado_! { F; x = e1; y = e2; z = e3; yield body }
// Expands to:
<F as Apply>::ap(
<F as Apply>::ap(
<F as Functor>::fmap(e1, |x| move |y| move |z| body),
e2,
),
e3,
)
4 bindings
// This:
ado_! { F; a = e1; b = e2; c = e3; d = e4; yield body }
// Expands to:
<F as Apply>::ap(
<F as Apply>::ap(
<F as Apply>::ap(
<F as Functor>::fmap(e1, |a| move |b| move |c| move |d| body),
e2,
),
e3,
),
e4,
)
Requirements
The type constructor F must implement Applicative (and therefore Apply and Functor). Unlike do_!, it does not require Chain -- applicative computations are strictly less powerful than monadic ones, which is the point: they express the absence of sequential dependencies.
Examples
OptionF -- single binding (fmap)
use karpal_std::prelude::*;
let result = ado_! { OptionF;
x = Some(5);
yield x * 2
};
assert_eq!(result, Some(10));
OptionF -- combining two independent values
use karpal_std::prelude::*;
let result = ado_! { OptionF;
x = Some(1);
y = Some(2);
yield x + y
};
assert_eq!(result, Some(3));
OptionF -- short-circuiting on None
use karpal_std::prelude::*;
let result = ado_! { OptionF;
x = Some(1);
y = None::<i32>;
yield x + y
};
assert_eq!(result, None);
OptionF -- combining three values
use karpal_std::prelude::*;
let result = ado_! { OptionF;
x = Some(1);
y = Some(2);
z = Some(3);
yield x + y + z
};
assert_eq!(result, Some(6));
OptionF -- combining four values
use karpal_std::prelude::*;
let result = ado_! { OptionF;
a = Some(1);
b = Some(2);
c = Some(3);
d = Some(4);
yield a + b + c + d
};
assert_eq!(result, Some(10));
VecF -- cartesian product with applicative
use karpal_std::prelude::*;
let result = ado_! { VecF;
x = vec![1, 2];
y = vec![10, 20];
yield x + y
};
assert_eq!(result, vec![11, 21, 12, 22]);
ResultF -- combining independent fallible lookups
use karpal_std::prelude::*;
let result = ado_! { ResultF<String>;
host = Ok::<&str, String>("localhost");
port = Ok::<u16, String>(3000);
yield format!("{}:{}", host, port)
};
assert_eq!(result, Ok("localhost:3000".to_string()));
Choosing Between do_! and ado_!
| Macro | Trait required | Bindings | Use when |
|---|---|---|---|
do_! |
Chain (Monad) |
Sequential -- later bindings can depend on earlier ones | Steps have data dependencies |
ado_! |
Applicative |
Independent -- bindings must not reference each other | Steps are independent; documents the absence of dependencies |
Why = Instead of <-?
Languages like Haskell and PureScript use <- for monadic bindings. Karpal uses = instead because Rust edition 2024 reserves the <- token, making it unavailable inside macros. The = syntax integrates naturally with Rust's existing patterns and avoids any conflict with reserved tokens.