Tangential addendum: privacy vs. fully hygienic names and paths
The following is a bit off-topic here, but it's tangential enough to warrant I mention, I'd say.
So, there is this experimental family of declarative macros, dubbed "macros 2.0" (the feature is called decl_macro
), which are macros defined with the macro
keyword rather than with the macro_rules!
"contextual keyword".
At first sight, both of these macros behave the same, except that rules for macro
macros are separated with ,
instead of ;
, and that macro
macros feature a handy single-rule shorthand syntax:
macro m( /* input */ ) {
/* expansion */
}
But there is actually a deeper difference between macro
and macro_rules!
macros: hygiene. Whilst macro_rules!
macros are "partially hygienic" (they use mixed_site
spans) so that local variables get to be hygienic, macro
macros are fully hygienic, so that even global items such as functions, types, const
ants, static
s, modules (and other macros) are affected by hygiene as well.
This makes it so macro
macros don't necessarily have to qualify, for instance, the types involved, in other to be robust.
-
This is what macro_rules!
macros have to do.
For instance, consider the following hand-rolled try_!
macro, with the original semantics of ?
:
macro_rules! try_ {( $e:expr $(,)? ) => (
match $e {
Result::Ok(it) => it,
Result::Err(err) => return Result::Err(err.into()),
}
)} use try_;
and now consider the following scenario:
type Result<T> = ::anyhow::Result<T>;
fn my_func() -> Result<()> {
let current_dir = crate::try_!( ::std::env::current_dir() );
/* … */
Ok(())
}
this spits:
error[E0308]: mismatched types
--> src/lib.rs:3:9
|
3 | Result::Ok(it) => it,
| ^^^^^^^^^^^^^^ expected struct `std::io::Error`, found struct `anyhow::Error`
...
12 | let current_dir = try_!( ::std::env::current_dir() );
| ----------------------------------
| | |
| | this expression has type `std::result::Result<PathBuf, std::io::Error>`
| in this macro invocation
|
= note: expected enum `std::result::Result<PathBuf, std::io::Error>`
found enum `std::result::Result<_, anyhow::Error>`
= note: this error originates in the macro `try_`
Indeed, the try_!
macro incorrectly failed to namespace the mention to Result
, a global and thus unhygienic "element", which means that the name is resolved at each call site rather than at the definition site; and the call site of my example is using a different definition of Result
, hence the error.
But if we change the definition of try_!
to be that of a macro
macro:
#![feature(decl_macro)]
macro try_ {( $e:expr $(,)? ) => (
match $e {
Result::Ok(it) => it,
Result::Err(err) => return Result::Err(err.into()),
}
)}
then everything Just Works™: Playground
Thanks to that, similar to type inference, we can finally export macros which refer to publicly unnameable types:
mod private {
pub
enum TypeUsedByMacro {}
}
pub
macro m() {
const _: () = {
let _: private::TypeUsedByMacro;
};
}
and then a downstream user can go and call m!();
without any problems whatsoever.
This is because even if the path private::TypeUsedByMacro
is unnameable outside of the containing module of private
thanks to privacy, the type itself is nevertheless pub
lic, and thus allowed to "escape" those module boundaries through non-path-based mechanisms. While type inference is one such mechanism, I wanted to showcase here that full hygiene is another such mechanism to escape path-based privacy.
And this is yet another example where a private type, however, won't be able to escape its containing module. Indeed, for instance, the following code:
#![feature(decl_macro)]
mod lib {
// pub(self) /* private */
enum TypeUsedByMacro {}
pub
macro m() {
const _: () = {
let _: TypeUsedByMacro;
};
}
}
lib::m!();
fails with:
error: type `TypeUsedByMacro` is private
--> src/lib.rs:10:17
|
10 | let _: TypeUsedByMacro;
| ^ private type
...
15 | lib::m!();
| ---------- in this macro invocation
|
= note: this error originates in the macro `lib::m`