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 :/
There was a problem hiding this comment.
My motivation here was to produce a somewhat realistic situation where this would occur--the most likely way macros might rely on ambiently-available names is via the standard library, but some consumers may violate that expectation. For some reason I thought there was a non-macro std::io::print in libstd, but the closest thing actually exposed is probably std::io::stdout().write_all(...).
But given that #![no_std] is not something folks are likely to be familiar with when they want to learn how to use macros, you're probably right that an example with two hypothetical crates is better.
And yeah, I don't think we can provide a working interactive example here because what's really happening is a cross-crate interaction. But I'll change the example to something closer to what you suggest.
There was a problem hiding this comment.
Having thought about this more, I think it might be best to split this slide in half. The first can demonstrate how the lack of hygiene for paths can make a macro refer to different items when invoked from different locations, and show how we might use absolute paths to try to avoid that situation. This can be done with multiple modules in a single crate. The second can show that even absolute paths do not solve this problem completely in the presence of multiple crates, and motivate/explain the $crate keyword. We kind of try to do this already, but the existing "what is hygiene" slide is really just introducing the concept without demonstrating how the semantics of Rust item paths interact with it.
| - [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.
There was a problem hiding this comment.
The nesting is semantically helpful to understand the structure of the information, but I agree and have a similar workflow when teaching--I often want to see how many slides I need to cover and which are coming up next. What if we made it possible (or the default) to expand all children?
There was a problem hiding this comment.
Maybe a conservative compromise for now is to just unindent the individual syn and quote slides.
There was a problem hiding this comment.
Sure, I think changing the summary to default to expanding all children would at least address my use case of wanting to see how many slides are left. I'm still inclined to avoid nesting regardless, I don't think it adds much value in practice and ultimately students experience the slides as a flat list, but I don't think it matters much either way as long as we have an easy way to visualize how many slides are left.
| 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