11 releases
0.3.2 | Aug 7, 2023 |
---|---|
0.3.1 | Aug 1, 2023 |
0.3.0 | Jul 31, 2023 |
0.2.4 | Jul 3, 2022 |
0.1.5 | Jun 21, 2022 |
#496 in Rust patterns
33 downloads per month
65KB
854 lines
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:
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());
}
By setting in Cargo.toml:
[dependencies]
methods-enum = "0.3.2"
this can be solved, for example, 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-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:
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
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:
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 macro #[gen()]
this is solved like this:
mod blog {
enum State {
Draft,
PendingReview,
Published,
}
pub struct Post {
state: State,
content: String,
}
#[methods_enum::gen(Meth, run_methods)]
impl Post {
pub fn add_text(&mut self, text: &str);
pub fn request_review(&mut self);
pub fn approve(&mut self);
pub fn content(&mut self) -> &str;
#[rustfmt::skip]
fn run_methods(&mut self, method: Meth) -> &str {
match self.state {
State::Draft => match method {
Meth::add_text(text) => { self.content.push_str(text); "" }
Meth::request_review() => { self.state = State::PendingReview; "" }
_ => "",
},
State::PendingReview => match method {
Meth::approve() => { self.state = State::Published; "" }
_ => "",
},
State::Published => match method {
Meth::content() => &self.content,
_ => "",
},
}
}
pub fn new() -> Post {
Post { state: State::Draft, content: String::new() }
}
}
}
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.