::with_locals
CPS sugar in Rust, to "return" values referring to locals.
Let's start with a basic example: returning / yielding a format_args
local.
use Display;
use with;
The above becomes:
use Display;
f: F
, here, is called a continuation:
instead of having a function return / yield some element / object,
the function takes, instead, the "logic" of what the caller would have liked
to do with that element (once it would have received it), so that it is the
callee who handles that object instead.
By shifting the logic like so, it is the callee and not the caller who runs that logic, which thus happens before the callee returns, so before it cleans its locals and makes things that refer to it dangle.
This is the whole point of all this strategy!
Now, to call / use the above function, one can no longer bind the "result"
of that function to a variable using a let
binding, since that mechanism
is reserved for actual returns, and the actual code running in the caller's
stack.
Instead, one calls / uses that with_hex
function using
closure / callback syntax:
with_hex
This is extremely powerful, but incurs in a rightward drift everytime such a binding is created:
with_hex
Instead, it would be nice if the compiler / the language provided a way
for let
bindings to magically perform that transformation:
let one = hex;
let two = hex;
let three = hex;
Operating in this fashion is called Continuation-Passing Style, and cannot be done implicitly in Rust. But that doesn't mean one cannot get sugar for it.
Enters #[with]
!
let one = hex;
let two = hex;
let three = hex;
-
This can also be written as:
let one: &'ref _ = hex; let two: &'ref _ = hex; let three: &'ref _ = hex;
That is,
let
bindings that feature a "special lifetime".
When applied to a function, it will tranform all its so-annotated
let
bindings into nested closure calls, where all the statements that
follow the binding (within the same scope) are moved into the
continuation.
Here is an example:
# use with;
#
The above becomes:
# use with;
#
Trait methods
Traits can have #[with]
-annotated methods too.
# use with;
#
Example of an implementor:
# use with;
#
#
# #
Example of a user of the trait (≠an implementor).
# use with;
#
// (Using a newtype to avoid coherence issues)
;
See examples/main.rs
for more detailed examples within a runnable file.
Usage and the "Special lifetime".
Something important to understand w.r.t. how #[with]
operates, is that
sometimes it must perform transformations (such as changing a foo()
call into
a with_foo(...)
call), and sometimes it must not; it depends on the semantics
the programmer wants to write (that is, not all function calls rely on CPS!).
Since a procedural macro only operates on syntax, it cannot understand such
semantics (e.g., it is not possible for a proc-macro to replace foo()
with with_foo()
if, and only if, foo
does not exist).
Because of that, the macro expects some syntactic marker / hints that tell it when (and where!) to work:
-
Obviously, the attribute itself needs to have been applied (on the enscoping function):
fn ...
- Note: if no override is provided,
#[with]
defaults to#[with('ref)]
.
- Note: if no override is provided,
-
Then, the macro will inspect to see if there is a "special lifetime" within the return type of the function.
// +-------------+ // | | // -------- V // vvvvvvvv ...'special...
That will trigger the transformation of
fn foo
intofn with_foo
, with all the taking-a-callback-parameter shenanigans.Otherwise, it doesn't change the prototype of the function.
-
Finally, the macro will also inspect the function body, to perform the call-site transformations (e.g.,
let x = foo(...)
intowith_foo(..., |x| { ... })
).These transformations are only applied:
-
On the
#[with]
-annotated statements:[with] let ...
; -
Or, on the statements carrying a type annotation that mentions the "special lifetime":
let x: ... 'special ... = foo;
-
Remarks
-
By default, the "special lifetime" is
'ref
. Indeed, sinceref
is a Rust keyword, it is not a legal lifetime name, so it is impossible for it to conflict with some real lifetime parameter equally named. -
But
#[with]
allows you to rename that lifetime to one of your liking, by providing it as the first parameter of the attribute (the one applied to the function, of course):use Display; use with;
Advanced usage
If you are well acquainted with all this CPS / callback style, and would just
like to have some sugar when defining callback-based functions, but do not want
the attribute to mess up with the code inside the function body (i.e., if
you want to opt-out of the magic continuation calls at return
sites & co.),
- for instance, because you are interacting with other macros (since they
lead to opaque code as far as
#[with]
is concerned, making it unable to "fix" the code inside, which may lead to uncompilable code),
then, know that you can:
-
directly call the
with_foo(...)
functions with hand-written closures.This is kind of obvious given how the functions end up defined, and is definitely a possibility that should not be overlooked.
-
and/or you can add a
continuation_name = some_identifier
parameter to the#[with]
attribute to disable the automaticreturn continuation(<expr>)
transformations;-
Note that
#[with]
will then provide asome_identifier!
macro that can be used as a shorthand forreturn some_identifier(...)
.This can be especially neat if the identifier used is, for instance,
return_
: you can then writereturn_!( value )
where a classic function would have writtenreturn value
, and it will correctly expand toreturn return_(value)
(return the value returned by the continuation).
-
Example
use Display;
use with;
// where
Powerful unsugaring
Since some statements are wrapped inside closures, that basic transformation
alone would make control flow statements such as return
, ?
, continue
and
break
to stop working when located in the scope of a #[with] let ...
statement (after it).
use Display;
use with;
And yet, when using the #[with] let
sugar the above pattern seems to work:
use Display;
use with;
-
This is achieved by bundling the expected control flow information within the return value of the provided closure:
for n in 0 ..
Debugging / Macro expansion
If, for some reason, you are interested in seeing what's the actual code
generated / emitted by a #[with]
attribute invocation, then all you have to
do is to enable the expand-macros
Cargo feature:
[]
# ...
= { = "...", = ["expand-macros"] }
This will display the emitted code with a style very similar to cargo-expand
,
but with two added benefits:
-
It does not expand all the macros, just the
#[with]
one. So, if within the body of a function there is something like aprintln!
call, the actual internal formatting logic / machinery will remain hidden and not clobber the code. -
Once the Cargo feature is enabled, a special env var can be used to filter the desired expansions:
WITH_LOCALS_DEBUG_FILTER=pattern
- This will then only display the expansions for functions whose name contains the given pattern. Note that this does not involve the fully qualified name (with the outer modules), it's the bare name only.
-
That being said, this only works when the procedural macro is evaluated, and
rustc
will try to cache the result of such invocations. If that's the case, all you have to do is perform some dummy change within the involved file, and save.