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 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 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
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
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.