A nice way to write macros with callbacks

⚓ Rust    📅 2025-10-22    👤 surdeus    👁️ 3      

surdeus

I 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

Read full topic

🏷️ Rust_feed