6 ms·
The new syntax for dynamically-dispatched traits, “dyn Trait”, is an example of Rust’s persistently excellent consideration of what should be explicit and what
by computerphage 8y ago
The new syntax for dynamically-dispatched traits, “dyn Trait”, is an example of Rust’s persistently excellent consideration of what should be explicit and what should be implicit. Python’s mantra of “explicit is better than Implicit” mostly captures my general feeling, but you can’t make everything explicit. Back before impl Trait existed, just Box<Trait> seemed very clear. Now that there’s an important thing to differentiate it from, it seems better to have dyn Trait and impl Trait both prefixed with keywords.
It also makes for much easier googling. ;)
- kbd 8y ago> Rust’s trait object syntax is one that we ultimately regret. Would someone please explain the problem (and the solution) for someone who doesn't know Rust yet?
- the_mitsuhiko 8y agoThis thread explains it quite well: https://www.reddit.com/r/rust/comments/8sqiin/newbie_question_why_is_the_impl_and_dyn_in_the/ https://www.reddit.com/r/rust/comments/8sqiin/newbie_questio...
- steveklabnik 8y agoThe linked thread is good, but the short answer: Box<Foo> If Foo is a struct, this is a single pointer to the heap. If Foo is a trait, this is a double pointer: a pointer to the data on the heap, and a pointer to a vtable for its methods. These two things are very different, but look similar. That's the mistake. This change separates the two. Now: Box<dyn Foo> is always the latter, a "trait object". We have to keep the old syntax working for stability reasons, but can lint against it, so that it nudges you into the better syntax. So in this world, // always a single pointer to a struct (or enum) Box<Foo> // always a double pointer Box<dyn Foo> This is more clear for humans. The compiler can tell with 100% fidelity, of course. But humans aren't compilers. Different things with different costs look different. As an addendum, the parent mentions another feature, "impl Trait", which can be used with traits. Their point was, originally, Box<Trait> was the only thing that existed like this. With "impl Trait", it was "impl Trait" vs "Trait", and is now "impl Trait" vs "dyn Trait", which is much more clear, at least in many people's opinions. :)
- KwanEsq 8y ago>We have to keep the old syntax working for stability reasons Will Rust 2018 enable you to remove the old syntax? Or is that a bigger change than the opt-in is allowed to cover?
- db48x 8y agoYes, the next edition will allow breaking changes, while still allowing you to use crates written in the older edition.
- steveklabnik 8y agosome forms of breaking changes. This is of that kind, though we cannot make arbitrary ones.
- steveklabnik 8y agoThis kind of change is the kind an edition can make, though I’m not 100% sure if the plan for 2018 is to warn or error on the older syntax.
- lewisinc 8y agoI feel like a warning for one edition and error on the next would make it straightforward as to how much time is left to update the code.
- steveklabnik 8y agoThat’s generally true, the question is, do you warn in 2015 or 2018? That being said, this particular fix is 100% automatable, and rustfix can already do it, so the time to update is “a few seconds”.
- dikaiosune 8y agoRust's built-in lints are configurable with compiler directives, so you'll be able to add #![deny(bare_trait_objects)] To the top of your crate and cause a compiler warning if someone uses the old syntax on your project. EDIT: just realized I mis-parsed "you" in parent. Oh well.
- algesten 8y agoA trait is like an interface. A struct implementing a trait means it takes on the methods of that interface (but there's more to it, because a trait can have default implementations of of the trait methods). If you want to express something like "this is a variable that holds something that fulfils this trait", without knowing the _actual_ type it is, that variable effectively has an unknown runtime size. std::io::Read is an interface for reading bytes of some source, like a file or a socket. This matters because we're talking about a stack frame. So the size needs to be known at compile time. let a: u64 = 42; // ok, because well known size. let b: Read = ...; // illegal, because unknown size. A "trait object" places the object on the heap and has a pointer in its place. let b: Box<Read> = ... // legal, because pointer is a known size However it's a bit more complicated, because this syntax allows for dynamic dispatch at runtime using a vtable. So there's a quite big difference between Box<u64> (a 64 bit unsigned integer on the heap) vs Box<Read> (a runtime dispatched lookup via a vtable). This difference is not obvious at a glance though. Hence the new syntax: Box<dyn Read>. (I think I got that right)
- steveklabnik 8y agoYou did, except that it’s not one pointer, it’s two. This is one way that rust is different than C++; the vtable isn’t stored with the data, but separately, with the “trait object” itself being two pointers to the two things.
- algesten 8y agoGood to know!
- tzahola 8y ago>This is one way that rust is different than C++; the vtable isn’t stored with the data Which “data” is the vtable stored with in C++? An object contains only a pointer to its vtable, not the vtable itself...
- steveklabnik 8y agoMy understanding is that, generally (because (again in my understanding) this is all compiler-specific, not mandated by the standard) is that in C++, there's a single pointer that points to the combo of the vtable and then the data, after it. See http://www.drdobbs.com/cpp/storage-layout-of-polymorphic-objects/240012098 http://www.drdobbs.com/cpp/storage-layout-of-polymorphic-obj... for example. While a Shape is a position, outline, and fill, if you have virtual functions, the pointer points to a vtable pointer, and then all of the data. In other words, in Rust, a pointer to a shape is (pointer to vtable, pointer to data) whereas, in C++, a pointer to a shape is (pointer to shape) where shape is (pointer to vtable, data) If that's incorrect, I'm quite happy to be corrected! This isn't an area I'm an expert in.
- weavie 8y agoReading up an static vs dynamic dispatch may help - https://en.wikipedia.org/wiki/Dynamic_dispatch https://en.wikipedia.org/wiki/Dynamic_dispatch. Essentially Box<Foo> is static dispatch. You know at compile time which exact methods you are going to call. Box<dyn Foo> is dynamic dispatch. Since Foo is a trait, at compile time you won't know which methods are called, it depends on the type of the object passed in (as long is it implements the Foo trait).
- TheCycoONE 8y agoYour example is misleading. If Foo is a trait Box<Foo> and Box<dyn Foo> are the same thing. Impl Foo is for static dispatch. Illustrating the need to dump the old unqualified syntax.
- weavie 8y agoAh ok, I'm back to being confused again then! Is this right: If Foo is a struct then Box<Foo> is static dispatch. If Foo is a trait Box<dyn Foo> is dynamic dispatch, but then so is Box<Foo> - but that is the regrettable point. So from now on, if Foo is a trait we should be writing Box<dyn Foo>, which is largely syntactic sugar to make the situation more clear? I don't fully understand what the release notes are saying about using impl Trait. When would you use Box<impl Foo> vs Box<dyn Foo>?
- steveklabnik 8y agoYou are right. It’s not that you would use Box<impl Foo>, but when taking about the two features, it’s easier to compare them, as they’re actually distinct syntactically.
- weavie 8y agoThankyou.
- sametmax 8y agoPython is my goto language but let's not forget it disrespects it's own mantra very often. Like a certain pirate would say, "it's more what you call a guideline than actual rules". Duck typing is implicit interface. Foo().bar() is an implicit Foo.bar(Bar()) as self is only explicit on declaration, not call. __init__ is called and passed parameters implicitly. __new__ is an implicit class method. Some time the magic is nice to have. Sometime it's like yield. Yield implicitly turns your function into a very different object, which makes everybody goes wtf the few first times. They fixed that with async / await, but I still wish we had a "gen def" to allow yield in bodies, like we have "async def".