top | item 18812114

Deriving Traits in Rust with Procedural Macros

65 points| naftulikay | 7 years ago |naftuli.wtf

29 comments

order

cbrewster|7 years ago

Great article, there aren't many resources for writing proc macros right now.

However, I think this could be done with a generic impl of WritableTemplate for all T where T: Template.

redshirtrob|7 years ago

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.

atombender|7 years ago

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.).

[1] https://blog.rust-lang.org/2018/12/21/Procedural-Macros-in-R...

majewsky|7 years ago

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.

reissbaker|7 years ago

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.

rhn_mk1|7 years ago

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?

[0] https://github.com/rhn/gpx-rust/blob/master/gpx_debug/src/li...

insertcredit|7 years ago

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.

microtonal|7 years ago

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.)

bpicolo|7 years ago

Procedural macros are something you more or less never need to write in application code, but they add tremendous power to libraries

sierdolij|7 years ago

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.

sierdolij|7 years ago

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.

pornel|7 years ago

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.

bsder|7 years ago

I found that I actually needed to buy the "Programming Rust" book, read it cover to cover, and then go back and start programming.

When I just tried to jump in, things didn't quite click.

throwmeawayjj|7 years ago

> 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.

dbaupp|7 years ago

> 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:

- 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

> Borrowing

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.