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
#![allow(unused)] fn main() { 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:
#![allow(unused)] fn main() { // 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:
#![allow(unused)] fn main() { 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
#![allow(unused)] fn main() { 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
#![allow(unused)] fn main() { 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)
#![allow(unused)] fn main() { use karpal_std::prelude::*; let result = do_! { OptionF; Some(42) }; assert_eq!(result, Some(42)); }
ResultF -- chaining fallible operations
#![allow(unused)] fn main() { 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)
#![allow(unused)] fn main() { 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
#![allow(unused)] fn main() { 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
#![allow(unused)] fn main() { // This: ado_! { F; x = expr; yield body } // Expands to: <F as Functor>::fmap(expr, |x| body) }
2 bindings
#![allow(unused)] fn main() { // 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
#![allow(unused)] fn main() { // 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
#![allow(unused)] fn main() { // 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)
#![allow(unused)] fn main() { use karpal_std::prelude::*; let result = ado_! { OptionF; x = Some(5); yield x * 2 }; assert_eq!(result, Some(10)); }
OptionF -- combining two independent values
#![allow(unused)] fn main() { 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
#![allow(unused)] fn main() { 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
#![allow(unused)] fn main() { 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
#![allow(unused)] fn main() { 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
#![allow(unused)] fn main() { 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
#![allow(unused)] fn main() { 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.
Karpal is licensed under Apache-2.0 + CLA. View on GitHub.