After several months of brain explosions, occuring each and every time I've tried to wrap my head around the macro_rules!
- I'm beginning to see the light at the end of the tunnel. Macros might not be the most beautiful thing to write, especially when it comes to repeatable patterns (gosh, those $()*
inside of other $()*
can be a real headache - literally), but they're definitely one of the most powerful features of Rust.
Case in point: I'm currently experimenting with Actix (the web framework), trying to piece together a coherent structure that could be used as a solid backbone for a project I have in mind - and one of the things that are obviously needed, are templates. Askama looked promising, so I've decided to rely on it. Compile-time generation seems to be in line with the overall Rust's theme of power and efficiency. However, no matter how powerful it is, the amount of code to write, necessary to write before one is able to to use this power is also quite an important factor to consider.
And with Askama, it's not exactly "streamlined":
#[derive(Template)]
#[template(path = "page.html")]
pub struct Page {
pub heading: String,
pub text: String
}
If we're experimenting with "quick and dirty" ways to generate pages, it also makes sense to create an implementation for inline strings:
impl Page {
pub fn new(heading: &str, text: &str) -> Self {
Self {
heading: heading.to_string(),
text: text.to_string()
}
}
}
Some of them might not be &str
, but regular String
-s instead. Therefore, generics.
use std::string::ToString as S;
impl Page {
pub fn new<A : S, B : S>(heading: A, text: B) -> Self {
Self {
heading: heading.to_string(),
text: text.to_string()
}
}
}
Which is wonderful, but if we have to repeat this same thing for each and every template to use, to say it's going to be tedious is to say nothing.
Let's bring macro_rules!
into the play, and suddenly all of the above becomes this:
templates![
"page.html" => Page { heading, text }
];
Which is achieved by nothing less than a magic incantation of the sort:
macro_rules! templates {
( $( $path:expr => $struct_name:tt { $( $field:tt ),* } ),* ) => {
$(
#[derive(Template)]
#[template(path = $path)]
pub struct $struct_name {
$(
pub $field: String
),*
}
#[allow(non_camel_case_types)]
impl $struct_name {
pub fn new< $($field : std::string::ToString),* >( $($field : $field),* ) -> Self {
Self {
$(
$field: $field.to_string()
),*
}
}
}
)*
}
}
The fact that 1. I was able to write it - and 2. It actually works - makes me feel incredible. However, there is something I'm not completely satisfied with.
To account for several possible fields, I had to create generic parameters for all of them, using the names of the fields as types: < $($field : std::string::ToString),* >
. It works just fine.
But it's not idiomatic in the slightest, and coupled with ( $($field : $field),* )
only adds to the confusion. Take the parameter, named "text", of the "text" type, and use it to generate the struct with its own "text" field filled with the value of the "text" parameter.
However, besides adding the #[allow(non_camel_case_types)]
attribute macro, it doesn't appear there is much to do.
If there was a possibility, in macro definitions, to manipulate the tokens themselves - the symbols that comprise the strings which represent them - during the macro generation process, it could have been solved easily. Take the token, stringify it, modify that string (capitalize it?) and plug it back into code.
The first 2 steps are trivial: stringify!($field)
does just that.
But there is no way to convert that string back into code.
Perhaps it's time to add codify!
macro to do just that?
P.S. Another extremely useful thing could be "conditional" macros. The $()*
allow to repeat the same process for more, than one time. But there's no way to evaluate whether something is true or false - whether inside the $()*
there are no items, 1 item, or more than 1, as a trivial example.
P.P.S. Are there other ways to accomplish the things just mentioned, that I'm not aware of?