State design pattern and other dynamic polymorphism are often solved with dyn Trait objects.
enum-matching is simpler and more efficient than Trait objects, but using it directly in this situation will "smear" the state abstraction over interface methods.
The proposed macros impl_match!{...} and #[gen(...)] provide two different ways of enum-matching with a visual grouping of methods by enum variants, which makes it convenient to use enum-matching in state design pattern and dynamic polymorphism problems.
impl_match! macro
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:
By setting in Cargo.toml:
[]
= "0.3.2"
this can be solved, for example, like this:
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-analyzer[^rust_analyzer] 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.
[^rust_analyzer]: 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" }
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 theenumdeclaration, in the example:@enum Shape {...disables passing to theenumcompiler: only match-arms will be processed. This may be required if thisenumis already declared elsewhere in the code, including outside the macro. -
If you are using
enumwith fields, then before the name of the method that uses them, specify the template for decomposing fields into variables (the IDE[^rust_analyzer] 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:
impl_match! // <--impl_match!
- Debug flags. They can be placed through spaces in parentheses at the very beginning of the macro,
eg:impl_match! { (ns )...- flag
nsorsnin any case - replaces the semantic binding of the names of methods and traits inenumvariants 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
Links
gen() macro
The macro attribute is set before an individual (non-Trait) impl block. Based on the method signatures of the impl block, it generates: enum with parameters from argument tuples and generates {} bodies of these methods with calling the argument handler method from this enum.
This allows the handler method to control the behavior of methods depending on the context, including structuring enum-matching by state.
Usage example
Let me remind you of the condition from chapter 17.3 "Implementing an Object-Oriented Design Pattern" of the rust-book. The following behavior is required:
with macro #[gen()] this is solved like this:
In the handler method (in this case, run_methods), simply write for each state which methods should work and how.
The macro duplicates the output for the compiler in the doc-comments. Therefore, in the IDE[^rust_analyzer], you can always see the declaration of the generated enum and the generated method bodies, in the popup hint above the enum name:


Syntax for calling a macro
For at most one return type from methods
#[methods_enum::gen(EnumName , handler_name]`
where:
- EnumName: The name of the automatically generated enum.
- handler_name: Handler method name
For more than one return type from methods
#[methods_enum::gen(EnumName , handler_name , OutName]
where:
- OutName: The name of an automatically generated enum with variants from the return types.
Links
The gen() macro loses out to impl_match! in terms of restrictions and ease of working with methods and their output values. The benefit of gen() is that it allows you to see the full match-expression and handle more complex logic, including those with non-trivial incoming expressions, match guards, and nested matches from substate enums.
License
MIT or Apache-2.0 license of your choice.
