Possible postfix macro use case: `.unwrap_dbg!(...)`

I recently wanted a version of .unwrap() that would allow me to include some bindings to print on failure the way that the dbg!(...) macro does. I imagined it being implemented with variadics:

fn unwrap_dbg(...&dyn Debug) -> T { ... }

But this wouldn't capture the argument names the way the dbg!() macro would. @yaahc reminded me that postfix macros are a proposal from the past which hasn't gotten traction yet, but which would pretty perfectly express this functionality.

This is roughly the code where I would have used it:

iterator.par_iter().map(|item| {
    let foo = foo(item);
    let bar = bar();
    let baz = fallible(foo, bar).unwrap_dbg!(foo, bar);
})

In this case, because I'm using rayon the panic messages might interleave with an actual dbg! invocation and it would be clearest if the panic message from the unwrap itself contained the values I want. This is obviously expressible more verbosely in many ways with today's tools, but this is the first time I've actually found myself wanting postfix macros so I thought I'd share in case it resonates.

2 Likes

Could you clarify what kind of panic error message you’d expect if this unwrap_dbg call fails? And also clarify what the meaning of the arguments foo and bar to the macros are? Does it take arbitrary expressions or names? What does this call (roughly) expand to? Why are we naming foo and bar in the macro arguments at a point that (at least feels like it) is after both foo and bar are moved into the fallible call. By the way, what’s the signature of fallible here? Returning an Option?

I think the current idiom would be to use Result::map_err or Option::ok_or/ok_or_else to stuff references to your values into the error, then unwrap that.

1 Like

The problem with postfix macros isn't that there aren't good use-cases for them, it's that there are several that all seem to conflict with each other with regards to what they want the syntax to mean. A naive reading of fallible(foo, bar).unwrap_dbg!(foo, bar) would lead one to believe fallible is evaluated, then passed to a macro, but macros don't take evaluated expressions, they take syntax. How much syntax do they take? What delimits it? Why are you making it look like you're calling fallible, when it is that macro that actually has to call it? What if you wrote fallible().something().unwrap_dbg!()? Or fallible().unwrap_dbg!().something()?

I think most people in favor of postfix macros have converged on the idea that postfix macros act like functions with respect to how it treats the receiver.

That is, conceptually, postfix macros would have a $self capture, and that captures the name of a temporary binding which the receiver is bound to. Example (using arbitrarily bad declaration syntax):

macro $self.unwrap_dbg!( $($tts:tt)* ) => { ... }

fallible(foo, bar).unwrap_dbg!(foo, bar);

// is semantically similar to

macro unwrap_dbg!( $self:ident, $($tts:tt)* ) => { ... }

match fallible(foo, bar) {
    __receiver => unwrap_dbg!(__receiver, foo, bar),
};

Anything else breaks the sanctity of "what code does" outside a macro invocation. Some trickery might be required such that $self refers to a temporary with the same semantics as if it hadn't been passed through a match rebind, but doing so seems feasible, and I plan to propose said semantics separately as a new capture type for macros.

14 Likes

While the semantic you describe (evaluate the receiver before the macro) would certainly be my preference as well, it's not accurate to say that people have converged on that. That is, in fact, one of the two primary points of dispute that have blocked postfix macros. (The other is whether they should use type-based dispatch or be defined globally for all receivers.)