A nice way to write macros with callbacks
⚓ Rust 📅 2025-10-22 👤 surdeus 👁️ 3I got into writing declarative macros recently and I think I found a nice way to pass around and modify macro callbacks.
The format I wanted for callbacks had to support partial function application, so intermediate macros can append or prepend arguments to a callback they receive and pass on.
I settled on: fn(pre... <> post...)...(args...). The marker <> marks where the next application's arguments will be inserted. The partial function applications ((pre... <> post...)) can be repeated which makes it easy for macros to add arguments to a callback and forward it.
const _: () = {
macro_rules! sum {
($($arg:expr),* $(,)?) => {
0 $(+ $arg)*
};
}
assert!(matches!(call!(sum!(1, <>)(<>, 3)(2)), 6));
macro_rules! zip {
([$([$($a:tt)*])*] [$([$($b:tt)*])*] $($cb:tt)*) => {
call!($($cb)*([$([$($a)* $($b)*])*]))
};
}
macro_rules! flatten {
([$([$($a:tt)*])*] $($cb:tt)*) => {
call!($($cb)*($($($a)*)*))
}
}
assert!(matches!(
zip!(
[[1] [, 3]]
[[, 2] [, 4]]
flatten!(<> sum!)
),
10
));
};
Here's the implementation of call!
#[doc(hidden)]
#[macro_export]
// [args] [input] [fn] [pre] [post]
macro_rules! __call__parse_application {
// no further input, this is the last application
([$($args:tt)*] [] [$($fn:tt)*] [$($pre:tt)*] [$($post:tt)*]) => {
$($fn)*($($pre)* $($args)* $($post)*)
};
// found the placeholder, continue with the next application (this musn't be the last)
([<> $($pre_post:tt)*] [($($args:tt)*) $($input:tt)*] $fn:tt $pre:tt [$($post:tt)*]) => {
$crate::__call__parse_application!([$($args)*] [$($input)*] $fn $pre [$($pre_post)* $($post)*])
};
// continue searching for the placeholder
([$head:tt $($tail:tt)*] $input:tt $fn:tt [$($pre:tt)*] $post:tt) => {
$crate::__call__parse_application!([$($tail)*] $input $fn [$($pre)* $head] $post)
};
}
#[doc(hidden)]
#[macro_export]
// [input] [fn]
macro_rules! __call__parse_fn {
// found the beginning of the first function application, start parsing them
([($($args:tt)*) $($input:tt)*] $fn:tt) => {
$crate::__call__parse_application!([$($args)*] [$($input)*] $fn [] [])
};
// continue searching for the first function application
([$head:tt $($tail:tt)*] [$($fn:tt)*]) => {
$crate::__call__parse_fn!([$($tail)*] [$($fn)* $head])
};
}
#[macro_export]
macro_rules! call {
($($input:tt)*) => {
$crate::__call__parse_fn!([$($input)*] [])
};
}
I'm wondering if it's worth publishing this as a crate and/or trying to add this technique to Declarative Macros - The Little Book of Rust Macros.
The more lightweight alternative is to represent callbacks as [[$(fn:tt)*] [$($pre:tt)*] [$($post:tt)*]] so it can be easily matched against where needed. However the callback in the final expression in the example would be harder to read and write:
#[macro_export]
macro_rules! call {
([[$($fn:tt)*] [$($pre:tt)*] [$($post:tt)*]]($($args:tt)*)) => {
$($fn)*($($pre)* $($args)* $($post)*)
};
}
// ...
assert!(matches!(
zip!(
[[1] [, 3]]
[[, 2] [, 4]]
[[flatten!] [] [[[sum!] [] []]]] // <-- call flatten with the result of zip and the callback sum
),
10
));
Depending on the project, the increased readability may be worth a few macro expansions.
P.S. I wasn't sure what category to post this under, maybe it should be a blog post instead but I just wanted to get peoples thoughts.
1 post - 1 participant
🏷️ Rust_feed