impl_match!() { /* proc-macro */ }
Expand description
This is an item-like macro that wraps a state enum
declaration and one or more impl
blocks, allowing you to write match-expressions without match-arms in the method bodies of these impl
, writing the match-arms into the corresponding enum
variants.
§Usage example
Chapter 17.3 “Implementing an Object-Oriented Design Pattern” of the rust-book shows the implementation of the state pattern in Rust, which provides the following behavior:
pub fn main() {
let mut post = blog::Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
post.request_review(); // without request_review() - approve() should not work
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
with the macro impl_match!
this is solved like this:
mod blog {
pub struct Post {
state: State,
content: String,
}
methods_enum::impl_match! {
impl Post {
pub fn add_text(&mut self, text: &str) ~{ match self.state {} }
pub fn request_review(&mut self) ~{ match self.state {} }
pub fn approve(&mut self) ~{ match self.state {} }
pub fn content(&mut self) -> &str ~{ match self.state { "" } }
pub fn new() -> Post {
Post { state: State::Draft, content: String::new() }
}
}
pub enum State {
Draft: add_text(text) { self.content.push_str(text) }
request_review() { self.state = State::PendingReview },
PendingReview: approve() { self.state = State::Published },
Published: content() { &self.content }
}
} // <-- impl_match!
}
All the macro does is complete the unfinished match-expressions in method bodies marked with ~
for all enum
variants branches in the form:
(EnumName)::(Variant) => { match-arm block from enum declaration }
.
If a {}
block (without =>
) is set at the end of an unfinished match-expressions, it will be placed in all variants branches that do not have this method in enum
:
(EnumName)::(Variant) => { default match-arm block }
.
Thus, you see all the code that the compiler will receive, but in a form structured according to the design pattern.
rust-analyzer1 perfectly defines identifiers in all blocks. All hints, auto-completions and replacements in the IDE are processed in match-arm displayed in enum
as if they were in their native match-block. Plus, the “inline macro” command works in the IDE, displaying the resulting code.
§Other features
-
You can also include
impl (Trait) for ...
blocks in a macro. The name of theTrait
(without the path) is specified in the enum before the corresponding arm-block. Example withDisplay
- below. -
An example of a method with generics is also shown there:
mark_obj<T: Display>()
.
There is an uncritical nuance with generics, described in the documentation. -
@
- character before theenum
declaration, in the example:@enum Shape {...
disables passing to theenum
compiler: only match-arms will be processed. This may be required if thisenum
is already declared elsewhere in the code, including outside the macro. -
If you are using
enum
with fields, then before the name of the method that uses them, specify the template for decomposing fields into variables (the IDE1 works completely correctly with such variables). The template to decompose is accepted by downstream methods of the same enumeration variant and can be reassigned. Example:
methods_enum::impl_match! {
enum Shape<'a> {
// Circle(f64, &'a str), // if you uncomment or remove these 4 lines
// Rectangle { width: f64, height: f64 }, // it will work the same
// }
// @enum Shape<'a> {
Circle(f64, &'a str): (radius, mark)
zoom(scale) { Shape::Circle(radius * scale, mark) } // template change
fmt(f) Display { write!(f, "{mark}(R: {radius:.1})") }; (_, mark)
mark_obj(obj) { format!("{} {}", mark, obj) }; (radius, _)
to_rect() { *self = Shape::Rectangle { width: radius * 2., height: radius * 2.,} }
,
Rectangle { width: f64, height: f64}: { width: w, height}
zoom(scale) { Shape::Rectangle { width: w * scale, height: height * scale } }
fmt(f) Display { write!(f, "Rectangle(W: {w:.1}, H: {height:.1})") }; {..}
mark_obj(obj) { format!("⏹️ {}", obj) }
}
impl<'a> Shape<'a> {
fn zoom(&mut self, scale: f64) ~{ *self = match *self }
fn to_rect(&mut self) -> &mut Self ~{ match *self {}; self }
fn mark_obj<T: Display>(&self, obj: &T) -> String ~{ match self }
}
use std::fmt::{Display, Formatter, Result};
impl<'a> Display for Shape<'a>{
fn fmt(&self, f: &mut Formatter<'_>) -> Result ~{ match self }
}
} // <--impl_match!
pub fn main() {
let mut rect = Shape::Rectangle { width: 10., height: 10. };
assert_eq!(format!("{rect}"), "Rectangle(W: 10.0, H: 10.0)");
rect.zoom(3.);
let mut circle = Shape::Circle(15., "⭕");
assert_eq!(circle.mark_obj(&rect.mark_obj(&circle)), "⭕ ⏹️ ⭕(R: 15.0)");
// "Rectangle(W: 30.0, H: 30.0)"
assert_eq!(circle.to_rect().to_string(), rect.to_string());
}
- Debug flags. They can be placed through spaces in parentheses at the very beginning of the macro,
eg:impl_match! { (ns )
…- flag
ns
orsn
in any case - replaces the semantic binding of the names of methods and traits inenum
variants with a compilation error if they are incorrectly specified. - flag
!
- causes a compilation error in the same case, but without removing the semantic binding.
- flag
§impl_match macro details
§Covered and processed code
The macro handles one enum
- first in order with @enum priority and unfinished match-expressions in all ~
-marked method bodies of all impl-blocks of one, first of impl struct or enum in the covered code.
Methods marked with ~
with a missing top-level unfinished match-expression (without =>
) are passed to the compiler unchanged and without ~
.
The relative position of enum
and impl
is not important. The processed enum
itself can also be the item of the processed impl
. Impl-blocks are processed both personal and impl (Trait) for
.
All other code covered by the macro is passed to the compiler unchanged, as if it were outside the macro.
§Ufinished match-expressions
Only one, first in order, incomplete match-expression (without =>
) is processed at the top level of each method body marked with ~
.
The input expression after the match
keyword must be of the type of the enum
being processed or its ref.
If an unfinished match-expression ends with {}
block without =>
, that block is considered the default match block for all enum
variants that do not reference the method containing that match-expression.
If an unterminated match-expression does not contain a default match-arm block, it must be the last one in the statement (ie closed with ;
), or the last one in the body of the method.
§Enum declaration with match-arms
As with the standard enum
declaration, the enum variants must be separated by commas ,
.
After the name of the enum
variant and its fields (if any), the methods involved in the variant are listed in arbitrary order. For each method processed, is specified in the following order:
- method name: without fn, pub, generic types, and output value type, but with parentheses after the name
()
, in which indicate the names of the method parameters in the form as when calling (without types and without self). - the name of Trait for cases when the Trait method is implemented. The Trait name must be specified without the path: use “use” if necessary.
- match-arm block in curly brackets
{}
, which, in fact, will be included in the match-expression of this method.
Method parameter names from enum
are not passed to the compiler and, generally speaking, can be omitted: parameter names in a match-arm block are semantically and compilably related only to the method signature in the impl-block being processed. But writing them here improves the readability of the blocks, and in the future, perhaps, semantic linking will be added to the macro for them.
For all methods not specified in the enum
variant: the resulting match-expression for this variant will output the default match-arm block if it is specified in the method’s match-expression. Otherwise, no match-arm will be generated for this variant, which will cause a standard compilation error.
Spaces and newlines and regular comments don’t matter.
Before the method name, any punctuation is allowed except for ,
in any amount for the purpose of visual emphasis. Usually just :
after the variant declaration is sufficient.
Attributes and doc-comments before enum
or its variants will be passed to the compiler unchanged.
Attributes and doc-comments before method names in enum
will be ignored.
§Using enum variants with fields
If field data is used in the match-arm block of a variant, before the method name, you must specify the template for decomposing fields into match-arm block variables in the same form as it will be specified in the match-expression.
The decomposition pattern propagates to subsequent methods of the same enum
variant, but it can be overridden on any method.
In the example above, decomposition templates are reassigned to ignore unused fields. Otherwise, to prevent the compiler from reporting unused
, one would either have to assign #[allow(unused)]
to the impl block, or use variable names prefixed with _.
§@-escaping enum
re-declaration
@-escaping is performed when it is required to describe in the macro match-arms of enum variants for enum
declared elsewhere in the code (for example, in another module, another impl_match!
macro, or declared separately because match-arm blocks sizes prevent displaying an enum
declaration on one screen).
Example: @enum State { ...
In this case, the macro works with match-arms as described above, but the declaration of the enum
itself will not be passed to the compiler from the macro.
If you wish to specify enum
attributes or pub
here, @
must precede them so that they are ignored to be passed to the compiler along with the enum
declaration.
An enum
escaped with @
takes precedence for processing by a macro over other enum
declarations in the same macro, regardless of order.
§Compiler messages, IDE semantics, and debugging flags
As previously reported, the compiler and IDE1 work flawlessly with identifiers included in the resulting method code, i.e. match-arms blocks and decomposition patterns in enum
variants.
The behavior of macro identifiers (other than simply ignored variant method parameter names) that are not portable from enum
to the resulting code, such as method and trait names, differs depending on the mode: release-mode or dev-mode, and for the latter - also in depending on debug flags.
§In release-mode
If a macro finds a mismatch between method and traits names in enum variants with signatures in impl blocks, it will generate a compilation error with a corresponding message.
§In dev-mode without debugging flags
The macro will create a hidden empty module with identifiers spanned with the names of methods and traits from the enum
variants, thus connecting them to the standard semantic analysis of the compiler and IDE.
The macro also performs its own search for inconsistencies, but instead of a compilation error, it only prints a message to the console during the commands cargo build
/run
/test
.
§This has the following advantages for method names and trait names from enum
variants:
- almost complete IDE support: highlighting specific errors and semantic links, tooltips, jump to definition, group semantic renaming
- the possibility of the “inline macro” command and in cases of partial reading of methods in the
enum
variants by the macro
§Currently, this mode has the following non-critical restrictions:
enum
, impl object and traits must qualify in macro scope without paths: make appropriateuse
declarations if necessary.- methods with generics (eg:
mark_obj
from the Shape example) do not support semantic connections: only errors in the name are highlighted.
§Debug Flags
They can be placed through spaces in parentheses at the very beginning of the macro,
eg: impl_match! { (ns )
…
- flag
ns
orsn
in any case - replaces the semantic binding of the names of methods and traits inenum
variants with a compilation error if they are incorrectly specified. Thus, the macro is brought to the behavior as in release-mode. This is worth doing if the IDE does not support proc-macros, or if you want to output the resulting code from the “inline macro” command without an auxiliary semantic module.
I do not rule out that in some case it is the auxiliary semantic module that will become the source of failure. In this case, thens
flag will remove the helper module along with the bug. If this happens, please kindly report the issue to github. - flag
!
- causes a compilation error in the same case, but without removing the semantic binding.
The!
flag can be used to view errors found by the macro itself rather than by the IDE’s semantic analysis without runningcargo build
.
§Links
rust-analyzer may not expand proc-macro when running under nightly or old rust edition. In this case it is recommended to set in its settings:
"rust-analyzer.server.extraEnv": { "RUSTUP_TOOLCHAIN": "stable" }
↩