Crates.io | methods-enum |
lib.rs | methods-enum |
version | 0.3.2 |
source | src |
created_at | 2022-06-12 20:49:49.830394 |
updated_at | 2023-08-07 14:22:30.573852 |
description | Two macros for easy implementation of 'state' design pattern and other dynamic polymorphism using enum instead of dyn Trait |
homepage | |
repository | https://github.com/vvshard/methods-enum |
max_upload_size | |
id | 604749 |
size | 88,612 |
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.
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.
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-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.
You can also include impl (Trait) for ...
blocks in a macro. The name of the Trait
(without the path) is specified in the enum before the corresponding arm-block. Example with Display
- 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 the enum
declaration, in the example: @enum Shape {...
disables passing to the enum
compiler: only match-arms will be processed. This may be required if this enum
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());
}
impl_match! { (ns )
...
flag ns
or sn
in any case - replaces the semantic binding of the names of methods and traits in enum
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.
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.
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 IDE1, you can always see the declaration of the generated enum
and the generated method bodies, in the popup hint above the enum name:
#[methods_enum::gen(
EnumName ,
handler_name]`
where:
#[methods_enum::gen(
EnumName ,
handler_name ,
OutName]
where:
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.
MIT or Apache-2.0 license of your choice.
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" }
↩ ↩2 ↩3