We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Macros, FFI and Type-Driven Design step 10 of 28
Callbacks and TT bundling
Pass one macro the name of another and let it do the invoking.
pub fn dispatch() -> (i64, i64) // (with_list!(sum_all, [1,2,3,4]), with_list!(max_all, [3,-7,9,2]))
pub fn bundled() -> (i64, i64) // (call_with!(sum_all, (10,20,30)), call_with!(max_all, (-1,-5,-3)))
sum_all! and max_all! are written. Your job is with_list! and call_with!.
The symptom this explains
Sooner or later you write something like
sum_all!(make_list!())
expecting make_list!() to expand into 1, 2, 3 and sum_all! to see three
arguments. It does not work, and the error is confusing.
The reason is that expansion is outside-in. When the compiler reaches
sum_all!(make_list!()), it expands sum_all! first, handing it the
unexpanded tokens make_list ! ( ). If sum_all!‘s matcher is
$($x:expr),*, that is one expression — a macro invocation is a perfectly good
expression — and the expansion becomes 0 + make_list!(), which then expands to
0 + 1, 2, 3 and fails to parse.
Nothing you can write in the inner macro fixes this. The outer macro decides what it does with its tokens, and by the time the inner one runs it is too late.
Inverting control
The fix is to turn the relationship around. Instead of the inner macro producing arguments for the outer one, the outer macro takes the name of the inner one and invokes it:
macro_rules! with_list {
($cb:ident, [$($x:expr),* $(,)?]) => {
$cb!($($x),*)
};
}
```
`$cb` is captured as an `ident` — just a name, no invocation, nothing expanded.
The transcriber then writes `$cb!(...)`, and *that* invocation is expanded on the
next pass, with the arguments already unpacked. `with_list!` decides the shape;
the caller decides what gets built from it.
This is the callback pattern, and it is everywhere in real macro crates. It is
how one macro can define a data format once and let many different macros consume
it.
## TT bundling
The second half is smaller and just as useful. A delimited group is **one token
tree** (18.8), so you can pass an entire argument list around as a single `tt`
and unpack it later:
```rust
macro_rules! call_with {
($cb:ident, $args:tt) => {
$cb! $args
};
}
```
`call_with!(sum_all, (10, 20, 30))` binds `$args` to the whole group
`(10, 20, 30)`, and `$cb! $args` emits `sum_all` `!` `(10, 20, 30)` — a complete
invocation, delimiters included. The macro never looked inside the bundle.
Bundling is how you thread arbitrary user syntax through several layers of macro
without each layer having to understand it.
::: question `stringify!` and `concat!` seem to be eager — `concat!("a", stringify!(b))` works. Doesn't that contradict "outside-in"?
It looks like it, and that appearance is exactly what misleads people.
`stringify!`, `concat!`, `include_str!`, `env!` and friends are **built-in**
macros implemented inside the compiler, not `macro_rules!` definitions. Some of
them explicitly expand their arguments before doing their work, because they need
a string literal and not a token stream. That behaviour is special-cased, and it
does not generalise.
Your own `macro_rules!` macro gets no such treatment. `outer!(inner!())` always
passes `outer!` the unexpanded tokens. The community name for the eager cases is
"eager expansion", and the fact that it exists for a handful of built-ins while
being unavailable to user macros is a long-standing wart — one of the recurring
arguments for reaching for a procedural macro instead (18.17).
Incidentally, clippy has a lint for the degenerate case: `useless_concat` fires
on a `concat!` whose arguments are all literals, because you could have just
written the string.
:::
## Your job
The starter's `with_list!` calls a *function* named `$cb`, which does not exist.
`call_with!` ignores its arguments entirely. Fix both.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.