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.)
Deriving typeclass implementations is something I do all the time in regular application code (in Scala), once you're used to it it gives you a lot of safety and expressive power. It sounds like Rust would benefit from some kind of record system / generic representation of traits (like we get from Shapeless in Scala) so that generic trait deriving could be written in normal code without needing macros.
Shapeless has one or two macros in its implementation, but as far as the rest of the ecosystem is concerned it might as well be part of the language. The point is that you can implement a custom typeclass and derivation of instances of that typeclass for struct-like ((possibly recursive) compositions of) sum/product types without ever having to write a custom macro.
It is entirely optional. I use Rust for two years now and work on a few things (biggest one around 10kLOC). I didn’t even read the Macro section in my books yet. Because I didn’t need to.
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.
RFCs cannot unwind the complexity on display here in this article, unless the Rust community would entertain a proposal for removing procedural macros entirely, which I assume is a non-starter.
The procedural macro feature of Rust is very simple:you write code that reads a token stream and writes a new token stream. This article might have done something unnecessarily complex, but that doesn't mean the feature is complex.
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.
is always destroyed by passing it to a function, but gets implicitly copied if (arg is copyable && arg is referenced below)
I am not sure what you are trying to say. This:
bar(a) # => bar(copy(&a)) # create-and-pass copy
baz(a) # last use, so dont bother copying
is not possible in Rust if a's type is not a copy type. a will be moved when calling bar, so trying to pass it to baz will result in a compiler error.
The story is quite simple: a value is always moved in a function call, unless it is a copy type (the type implements the Copy trait). When the type is a copy type, a bit-wise copy is made. References are not special: immutable references are copy types. Mutable references are not copy types (otherwise, they could be aliased).
You can find an overview of all copy types in the standard library in the implementations section of the Copy trait:
> I am not sure what you are trying to say. [Couple paragraphs rephrasing what I said]
Yep, that's what I was trying to say. (Although I'd hadn't remembered that the Copy trait actually enforced bitwise-exact-copies-only, because why would you want (implicit) non-exact copies.)
However, I think this could be done with a generic impl of WritableTemplate for all T where T: Template.