Agreed. I was anxiously waiting for proc macros to land on stable for quite a while and was very happy when they did.
But, when the time came to implement my custom derive I had to consult many sources. I ended up piecing together what I needed from a combination of:
- Official documentation (The Book)
- Blog posts
- Reading Serde code (And Syn/Proc-Macro/Proc-Macro2)
I also found the introduction of the proc-macro2 shim crate (however well intentioned) caused quite a bit of confusion. Specifically, it wasn't clean if I should use proc-macro or proc-macro2, and if I should be using the TokenStream exported by the former or the latter. Or should I be using one in some cases, and the other in some cases. Ditto for a few other things that I just can't recall right now.
I did get things put together eventually, but I don't feel I understand things well enough to explain to someone else...yet.
All that said, it's a hugely powerful feature and well worth the time if you need to do things that require intimate knowledge of the AST.
That bugged me, too -- if your entire article is about using a particular tool, then the reason for using that tool should be rock solid.
I found the linked article [1] much more helpful in explaining procedural macros, even though it didn't offer many reasons to use them (other than linking to Serde etc.).
Yeah, I dropped out after three paragraphs thinking that the author is waaay overdoing it. If this is just a toy example, it should be labeled as such.
I'm a Rust fan and procedural macros are legitimately a cool feature, but the example seems much more simply solved via inheritance. You could do this without procedural macros by using a default method definition on the WritableTemplate trait, since WritableTemplate inherits from Template and should have the render method in scope.
This could just be a slightly contrived example to show the neat kinds of things you can do with procedural macros, though. If you needed a reference to the original struct definition, for example, procedural macros allow you to do the kinds of transformations at compile time that other languages need runtime reflection for.
I'm a little confused about the statement that procedural macros are new in the 2018 edition.
I've always stuck to what Fedora was shiping, and they seem to be shipping the stable versions. Yet I used procedural macros back in 2016 already [0]. Does my memory fail me, or is there some other change that happened now?
Rust is morphing into a complexity beast that rivals C++. When the cognitive load require to read and write Rust code far exceeds that required of other, more popular languages, the future does not look rosy.
This may be true, but is not a very constructive comment without pointing out which language features or interactions between language features you find complex.
Ownership and the borrows checker may have a steep learning curve, but are not very complex. The rules are quite simple, the learning curve is steep because most programmers do not typically think about ownership (though they should).
(In my experience in teaching Rust, things like trait impl coherency rules, object safety, and finding a good balance between static and dynamic polymorphism are much harder for students than understanding the ownership system.)
Maybe. It has a lot of practical innovations and constraints but some inconsistencies and rough edges that will likely be addressed. Perhaps programming languages need the freedom to try things, make mistakes and then use feedback with an RFC process, which Rust has, to make improvements. Feel free to submit RFCs if you notice anything specific.
Lifetimes - implicit/explicit semantics for how long a name is considered alive, whereas in C++ there would be a delete or falling out of scope.
Borrowing - I still don't understand how or why a non-reference is implicitly consumed by passing it (read-only intention) by value(?) to another function and then can't be used again. Pony does explicit consumption.
There seems to be a need for training classes in Rust that explain the development philosophy, because it's not readily apparent from the online resources to anecdotal me who's able to code in Haskell, Elxir, Erlang, Clojure, C, C++17, Ruby, Python, Go, assembly and LLVM IR.
Rust doesn't have syntax for passing by value vs passing by reference. The syntax you're thinking of is instead for lending vs moving, e.g. moving `Box<T>` is still passing values by reference (but owned), and `&str` is a small copyable struct passed by value (but borrowed).
It's not the surprise-copy-horror you'd expect, because there are no copy constructors, and nothing large is ever copied implicitly (you have to call `.clone()` or implement `Copy` trait for a type).
The move semantics can ensure there exists only one owning pointer to each object. It can be statically known who owns the object, and most importantly, who has to free it.
> I still don't understand how or why a non-reference is implicitly consumed by passing it (read-only intention) by value(?) to another function and then can't be used again.
By default the value is moved in Rust, just like with std::move in C++17, which you say you know. This is to avoid performance issues when you pass complex structures, such as vectors, around. If you want to copy your value, you have to call .copy() explicitly.
> Lifetimes - implicit/explicit semantics for how long a name is considered alive, whereas in C++ there would be a delete or falling out of scope.
In Rust there's a move or a falling out of scope. Lifetimes are passive, and just describe the connection between references so that the compiler can check that they don't become dangling when values are destroyed in essentially the same places as C++ would destroy them.
> Borrowing - I still don't understand how or why a non-reference is implicitly consumed by passing it (read-only intention) by value(?) to another function and then can't be used again. Pony does explicit consumption.
Rust had a more pony-like model with annotations and different modes many years ago, but the model was unnecessarily complicated and was simplified to a "everything is pass-by-value" one:
- A "move" is a memcpy (bitwise copy) of a value to a new location, where the source becomes inaccessible at compile time
- A "read-only" T is a separate type &T, which is a value in its own right (and &_ implements Copy, so can be passed-by-value multiple times without explicit copies).
- Every value of type T is always a fully-fledged T, with all of T's operations available
- Parameters of type T (e.g. fn f(x: T)) are thus full values
- For an arbitrary type T, there's no way to (implicitly) copy values of type T, so the only way to call a function with a T parameter is to have the callee take ownership/responsibility for the caller's T value (i.e. move it into the call)
The way I think about this (which isn't quite how rust seems to) is that every value (including references) is always destroyed by passing it to a function, but gets implicitly copied if (arg is copyable && arg is referenced below). Eg:
T a = mkT() # create value
foo(&a) # create and immediately destroy/pass reference
bar(a) # => bar(copy(&a)) # create-and-pass copy
baz(a) # last use, so dont bother copying
For noncopyable values, this makes perfect sense; the callee got a value, so that value must have been moved out of the caller's variable. Treating copyable values the same way modulo the existence of a (T const ref -> T) copy function is just good consistency/orthogonality.
cbrewster|7 years ago
However, I think this could be done with a generic impl of WritableTemplate for all T where T: Template.
redshirtrob|7 years ago
But, when the time came to implement my custom derive I had to consult many sources. I ended up piecing together what I needed from a combination of:
I also found the introduction of the proc-macro2 shim crate (however well intentioned) caused quite a bit of confusion. Specifically, it wasn't clean if I should use proc-macro or proc-macro2, and if I should be using the TokenStream exported by the former or the latter. Or should I be using one in some cases, and the other in some cases. Ditto for a few other things that I just can't recall right now.I did get things put together eventually, but I don't feel I understand things well enough to explain to someone else...yet.
All that said, it's a hugely powerful feature and well worth the time if you need to do things that require intimate knowledge of the AST.
atombender|7 years ago
I found the linked article [1] much more helpful in explaining procedural macros, even though it didn't offer many reasons to use them (other than linking to Serde etc.).
[1] https://blog.rust-lang.org/2018/12/21/Procedural-Macros-in-R...
majewsky|7 years ago
reissbaker|7 years ago
This could just be a slightly contrived example to show the neat kinds of things you can do with procedural macros, though. If you needed a reference to the original struct definition, for example, procedural macros allow you to do the kinds of transformations at compile time that other languages need runtime reflection for.
rhn_mk1|7 years ago
I've always stuck to what Fedora was shiping, and they seem to be shipping the stable versions. Yet I used procedural macros back in 2016 already [0]. Does my memory fail me, or is there some other change that happened now?
[0] https://github.com/rhn/gpx-rust/blob/master/gpx_debug/src/li...
insertcredit|7 years ago
microtonal|7 years ago
Ownership and the borrows checker may have a steep learning curve, but are not very complex. The rules are quite simple, the learning curve is steep because most programmers do not typically think about ownership (though they should).
(In my experience in teaching Rust, things like trait impl coherency rules, object safety, and finding a good balance between static and dynamic polymorphism are much harder for students than understanding the ownership system.)
bpicolo|7 years ago
sierdolij|7 years ago
sierdolij|7 years ago
Borrowing - I still don't understand how or why a non-reference is implicitly consumed by passing it (read-only intention) by value(?) to another function and then can't be used again. Pony does explicit consumption.
There seems to be a need for training classes in Rust that explain the development philosophy, because it's not readily apparent from the online resources to anecdotal me who's able to code in Haskell, Elxir, Erlang, Clojure, C, C++17, Ruby, Python, Go, assembly and LLVM IR.
pornel|7 years ago
It's not the surprise-copy-horror you'd expect, because there are no copy constructors, and nothing large is ever copied implicitly (you have to call `.clone()` or implement `Copy` trait for a type).
The move semantics can ensure there exists only one owning pointer to each object. It can be statically known who owns the object, and most importantly, who has to free it.
bsder|7 years ago
When I just tried to jump in, things didn't quite click.
throwmeawayjj|7 years ago
By default the value is moved in Rust, just like with std::move in C++17, which you say you know. This is to avoid performance issues when you pass complex structures, such as vectors, around. If you want to copy your value, you have to call .copy() explicitly.
dbaupp|7 years ago
In Rust there's a move or a falling out of scope. Lifetimes are passive, and just describe the connection between references so that the compiler can check that they don't become dangling when values are destroyed in essentially the same places as C++ would destroy them.
> Borrowing - I still don't understand how or why a non-reference is implicitly consumed by passing it (read-only intention) by value(?) to another function and then can't be used again. Pony does explicit consumption.
Rust had a more pony-like model with annotations and different modes many years ago, but the model was unnecessarily complicated and was simplified to a "everything is pass-by-value" one:
- http://smallcultfollowing.com/babysteps/blog/2011/12/08/why-...
- http://smallcultfollowing.com/babysteps/blog/2012/10/01/move...
Things to know:
- Rust has no copy constructors (but does have a "this can be safely semantically duplicated by memcpy" marker trait (Copy), which cannot run arbitrary code. See https://stackoverflow.com/a/31013156/1256624 and https://stackoverflow.com/a/24253573/1256624.)
- A "move" is a memcpy (bitwise copy) of a value to a new location, where the source becomes inaccessible at compile time
- A "read-only" T is a separate type &T, which is a value in its own right (and &_ implements Copy, so can be passed-by-value multiple times without explicit copies).
- Every value of type T is always a fully-fledged T, with all of T's operations available
- Parameters of type T (e.g. fn f(x: T)) are thus full values
- For an arbitrary type T, there's no way to (implicitly) copy values of type T, so the only way to call a function with a T parameter is to have the callee take ownership/responsibility for the caller's T value (i.e. move it into the call)
a1369209993|7 years ago
The way I think about this (which isn't quite how rust seems to) is that every value (including references) is always destroyed by passing it to a function, but gets implicitly copied if (arg is copyable && arg is referenced below). Eg:
For noncopyable values, this makes perfect sense; the callee got a value, so that value must have been moved out of the caller's variable. Treating copyable values the same way modulo the existence of a (T const ref -> T) copy function is just good consistency/orthogonality.