Add initial version of macros course - #3265
fw-immunant wants to merge 2 commits into
Conversation
c0c3d7c to
3636813
Compare
19049a2 to
de5f466
Compare
This covers declarative and procedural macros, and should be a good complement to the fundamentals course
|
Wow, this is exciting! I am pretty far from the course now, and will not have the time to review. Sorry! |
randomPoison
left a comment
There was a problem hiding this comment.
I think this is off to a good start! I have various suggestions, but most of them are pretty minor. I think there's room to further flesh out the proc-macro section, especially with more exercises, but I don't think we need to iterate on that in this PR.
For the new exercises, it would be good to split the code into their own files and then pull sections into the slides with #include, that way the exercise and solution code stay in sync. At least that's how we do it for the Fundamentals exercises (and you did this already for the proc macro exercise).
| - Explain that `#[derive(Debug)]` generates an implementation of | ||
| `std::fmt::Debug` for the struct. | ||
| - Note that derive macros can also support "helper attributes" (attributes | ||
| placed on struct fields, like `#[serde(rename = "name")]`), which configure |
There was a problem hiding this comment.
A better example might be the #[default] attribute from the Default derive, which is used to specify the default variant for an enum. The example code could probably also be changed to demonstrate and enum with that attribute.
| } | ||
| ``` | ||
|
|
||
| - Because macro output is inserted as an AST subtree, we don't need to wrap |
There was a problem hiding this comment.
| - Because macro output is inserted as an AST subtree, we don't need to wrap | |
| Because macro output is inserted as an AST subtree, we don't need to wrap |
| `,`, `:`, etc.) can be sequenced to design readable and mnemonic input syntax | ||
| for your macros. | ||
|
|
||
| Note, this macro invokes itself recursively in its transcriber body. |
There was a problem hiding this comment.
This line would be better as a speaker note, mostly just to keep the slide a bit more compact.
| There is overlap between a number of these specifiers, e.g. we could write our | ||
| own pattern for blocks using `stmt` instead of using `block` directly. In | ||
| general, try to reach for the most semantically appropriate fragment specifier | ||
| where possible, which will catch more syntactic edge cases and integrate more | ||
| seamlessly into the language. |
There was a problem hiding this comment.
I think this bit could be a speaker note.
There was a problem hiding this comment.
Do you think it'd be useful here to talk about making the trailing separator optional? The way the example is written you can't have a trailing ,. If the matcher were changed to $( $val:expr, )+ then a trailing separator would be mandatory. You can make the trailing comma optional by making the matcher $( $val:expr ),+ $(,)?, which is a little cumbersome but makes the macro syntax more flexible.
If this isn't the right place to call that out, maybe we could have a slide for this in the "declarative macro techniques" section?
There was a problem hiding this comment.
I think this is worth a speaker note on the repetition slide. Will add.
| fn main() { | ||
| my_macros::print_something!() | ||
| } | ||
| ``` |
There was a problem hiding this comment.
This example is very confusing to me:
- It references
std::io::print, which doesn't appear to be a thing. - Uses
#[no_std], which is something students may not understand but would be awkward to explain here. - It tries to illustrate that using
#[no_std]in a downstream crate would change whatstdmeans in the macro, except the macro only referencesmy_macro_helper. The speaker note somewhat explains what's going on here, but I think the code example needs to illustrate that better.
I think the better example would be to have two crates that define the same function, and show that using $crate allows the macro to always reference the one from the crate where the macro is defined.
Alternative example code
Crate A, which defines a function and a macro that references the function:
pub fn print() {
println!("Hello from crate A");
}
macro_rules! print_something {
() => {
// With $crate this will always reference `print` in this
// same crate, without it the macro will call whatever
// `print` is in scope where the macro is invoked.
$crate::print()
};
}Crate B, which defines its own print and uses the macro from crate A:
use crate_a::print_something;
pub fn print() {
println!("Hello from crate B");
}
fn main() {
print_something!();
}We'd then show that the program prints "Hello from crate A", and if $crate is removed then it'll print "Hello from crate B". Not sure if we can actually demonstrate that in the slides though, since it involves multiple crates interacting :/
| - [Writing Procedural Macros](macros/proc-macros/writing.md) | ||
| - [Dependencies](macros/proc-macros/writing/deps.md) | ||
| - [The `proc_macro` Crate](macros/proc-macros/writing/deps/proc_macro.md) | ||
| - [The `proc_macro2` Crate](macros/proc-macros/writing/deps/proc_macro2.md) | ||
| - [The `syn` and `quote` Crates](macros/proc-macros/writing/deps/syn-quote.md) | ||
| - [The `syn` AST](macros/proc-macros/writing/deps/syn-ast.md) | ||
| - [The `quote!` macro](macros/proc-macros/writing/deps/quote-macro.md) |
There was a problem hiding this comment.
| - [Writing Procedural Macros](macros/proc-macros/writing.md) | |
| - [Dependencies](macros/proc-macros/writing/deps.md) | |
| - [The `proc_macro` Crate](macros/proc-macros/writing/deps/proc_macro.md) | |
| - [The `proc_macro2` Crate](macros/proc-macros/writing/deps/proc_macro2.md) | |
| - [The `syn` and `quote` Crates](macros/proc-macros/writing/deps/syn-quote.md) | |
| - [The `syn` AST](macros/proc-macros/writing/deps/syn-ast.md) | |
| - [The `quote!` macro](macros/proc-macros/writing/deps/quote-macro.md) | |
| - [Writing Procedural Macros](macros/proc-macros/writing.md) | |
| - [Dependencies](macros/proc-macros/writing/deps.md) | |
| - [The `proc_macro` Crate](macros/proc-macros/writing/deps/proc_macro.md) | |
| - [The `proc_macro2` Crate](macros/proc-macros/writing/deps/proc_macro2.md) | |
| - [The `syn` and `quote` Crates](macros/proc-macros/writing/deps/syn-quote.md) | |
| - [The `syn` AST](macros/proc-macros/writing/deps/syn-ast.md) | |
| - [The `quote!` macro](macros/proc-macros/writing/deps/quote-macro.md) |
I prefer to avoid deep nesting of slides like this. While teaching I often look at the table of contents to remind myself how many slides I have left, which helps with time management. Nesting slides like this somewhat interferes with my ability to do that quickly.
If you think it's worth de-nesting this a bit, then I think the morning slides could be structured a bit more flatly as well.
| use syn::{DeriveInput, parse2}; | ||
|
|
||
| fn derive_display_impl(input: TokenStream) -> TokenStream { | ||
| // ANCHOR-END: Derive |
There was a problem hiding this comment.
| // ANCHOR-END: Derive | |
| // ANCHOR_END: Derive |
| // Generate the Display implementation using quote! | ||
| quote! { | ||
| impl std::fmt::Display for #name { | ||
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| write!(f, "{}", #name_str) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
This example solution doesn't do anything with the fields of the struct, is that intentional? I would think it would be better to have students actually handle the fields, since that'd also require them to handle repetitions in quote!.
| Since we are running this in a single-file environment without a separate | ||
| `proc-macro = true` crate setup, we will write a normal Rust function that takes | ||
| simulated token streams (using `proc_macro2` and `quote!`), parses them with | ||
| `syn`, generates the output with `quote!`, and asserts that the generated code | ||
| is correct. |
There was a problem hiding this comment.
I don't love this setup :/ I think it'd be better to have students actually setup a local workspace with a separate crate for the proc macro, and then test the proc macro's implementation by using the derive in actual code.
That said, I'm not sure there's a good way give students that setup, since we're somewhat limited by the fact that we give students the starting code in the slides. Maybe we just give students the test code and have them setup the local crate structure? e.g. we give them something like:
#[derive(Display)]
struct MyAwesomeType {
field: i32,
}
fn main() {
let my_type = MyAwesomeType { field: 123 };
let string = my_type.to_string(); // Goes through `Display`.
assert_eq!(my_string, "MyAwesomeType: 123"); // Or whatever we want the output string to be.
}And then we have them setup the crates and proc macro to get that to compile and run.
This covers declarative and procedural macros, and should be a good complement to the fundamentals course