Abstraction without overhead: traits in Rust
blog.rust-lang.org
blog.rust-lang.org
This seems to have one major downside (pointers are twice as big), but lots of upsides:
- allows traits to be implemented for existing types, as opposed
to C++ where the type's declaration has to list all base classes.
- allows a type to be used through dynamic dispatch while allowing users
who don't need this to avoid the vtable overhead.
- one less indirection in the call sequence for dynamic dispatch.
I like it a lot overall, though the idea of 16-byte pointers on 64-bit architectures does make me slightly queasy.[1]: http://www.phpcompiler.org/articles/virtualinheritance.html
Edit: I don't mean to bash C++ here, BTW; the skinny-pointer approach has a lot of benefits when all you need is single inheritance (and there are early-stage proposals to add it to Rust too). But I don't think it works well for multiple inheritance.
I am looking for a Rust tutorial. It looks like there was one, but it was deprecated in favour of 'the book'. But 'the book' doesn't seem to have a tutorial yet.
Are there any tutorial's running through how to build some small piece of working software.
I found the Golang tutorial, where you build a very basic blog extremely enjoyable. Does Rust have anything similar?
https://golang.org/doc/articles/wiki/
Thanks
error: only the builtin traits can be used as closure or object bounds [E0225]- They auto-implement on types that have the appropriate methods (a feature that probably is unwanted in Rust) - They can be runtime downcasted via `.(type_name_here)` - They can be runtime type switched via `switch foo.(type)`
I'm not sure of the vtable internals though.
I'm not an expert in Rust or Haskell, though, so I welcome corrections.
I'm not sure exactly what you mean, but my naive interpretation is that you can:
Prelude> :t map show
map show :: Show a => [a] -> [String] data Showable = forall a. Show a => Showable a
This allows for [Showable 1, Showable "foo", Showable 'x']
(It requires the ExistentialQuantification language feature.)Which seems to me to be effectively the same as `Barrable` in this case, although it's more generic.
Which is exactly what you said in the first place!
> So you can have a list of Box<Show> in Rust, but you can't have a list of Show in Haskell.
But isn't this comparing two different things? You can't have a list of trait/typeclass in either language (excuse the pseudo-syntax):
Rust: [Bar]
Haskell: [Bar a => a]
But you can (with some extensions in Haskell) have: Rust: [Box<Bar>]
Haskell: [Box Bar]The difference being that in your code you can only put in a single type at a time, eg [Int] or [String], but not both Int and String under the common Show interface type.
>- allows traits to be implemented for existing types, as opposed
> to C++ where the type's declaration has to list all base classes.
>- allows a type to be used through dynamic dispatch while allowing users
> who don't need this to avoid the vtable overhead.
In C++ you can have this as well. For example, std::function is able to wrap any callable type without them having to have any base classes. Sean Parent gives a great talk about this: http://channel9.msdn.com/Events/GoingNative/2013/Inheritance...You can do the same in C, for that matter. Or in assembly.
The reason we still come up with new languages is so that you can write code that conforms to certain patterns easily.
You cannot, for instance, create a new abstract base class and have a pre-existing class in someone else's header implement your abstract base class, such that a pointer to that pre-existing class can be dynamic_cast to your class.
Because that statement refers to C++ semantics, not your ability to compute things, it's not affected by the claim of Turing-completeness.
Sure you can, for some value of base class and cast. You might not be able to use particular keywords, but data is data and math is math. You might not be able to use the built-in type system to do it.
C and javascript programmers write their own inheritance-based type systems all the time. And anything you can do in C, you can do in C++ if you really want to.
In the case of C, yes you can do that, and where performance is critical you might prefer to do it, but it's obviously not going to be your first choice if you have any sense.
However, I'd say Rust traits are more elegant in unifying what are two separate worlds in C++:
(1) calling virtual methods on a base class pointer, which can be (usually is) dispatched at runtime, but thus can't support methods with generic parameters or having a 'virtual type' (instead of a method), and
(2) accessing members of template parameter classes, including (possibly generic) methods, constants, typedefs, etc. - much more flexible, but doesn't work at runtime. Also dynamically typed, for better or worse; Rust thinks worse.
In Rust, (1) is a trait object, and (2) can be done with a generic parameter specified to implement a certain trait with methods, associated types, and constants. Rust's compiler doesn't try to be magic (unlike, say, Haskell), and so traits with generic methods and such can't be made into trait objects, but they're still traits - they feel like the same basic kind of thing.
Though the added complexity could offset the 16-to-8 byte size reduction.
It should be possible as a compiler option if you are compiling every object that you link to and statically linking them into a single binary. Even then there's be a performance hit.
object pointer -> load vtable -> load function pointer
while a compressed fat pointer would be like:
fat pointer -> shift to extract vtable index -> shift to make into a global table offset -> load global table base from a global variable -> load function pointer
Two loads in both cases, but in the compressed case the first one will almost always be cached; more code bloat, but not that much, and you save a bit of memory in the objects themselves.
With the C++ approach the data is one pointer away but the vtable is two pointers away.
With the Rust approach both data and vtable are just one pointer away.
Uh, what? How is the compiler just supposed to magically know which of the structures in the array are of what type, without any additional identifying information? I'm assuming that in this optimized case, there's a hidden type field in each struct, that it would use to index into a table of vtable pointers? If so, there you go, that's yet another level of indirection.
It usually is smarter for performance to do the "data oriented design" thing and break the heterogeneous arrays into separate homogeneous arrays, so that you can potentially avoid a few levels of indirection, hoist loop invariants out, and maybe even make use of SIMD. But the whole point of the conversation was to talk about a nontrivial abstraction that (supposedly) trades performance for clarity. So I gave a scenario that would exercise that overhead.
If objects have the same type, which is statically known, there is no need for virtual functions because the compiler can resolve the specific method implementation at compile-time.
> or you could make a union of all applicable objects (thus guaranteed to be constant sized, at the size of the largest object) and store those in the array/vector, switching on a type enum
You can do this in both C++ and Rust easily, with equivalent efficiency in both. In Rust you would just use an enum type. This design is generally highly discouraged though, because it requires callers to be aware of all possible "derived classes".
My comments were only about the case where you are using true language-level polymorphism.
No, it's assuming that a "full pointer" is the only way to get a reference to a polymorphic object. This is true in both C++ and Rust.
> What about if you want to stick a bunch of objects in a vector, and iterate over them linearly?
You can do this in both C++ and Rust. But you can only put a bunch of objects in a vector if they have a statically-known type. The point of dynamic dispatch is calling methods of an object where you don't statically know its type.
You revert to the OOP-style vtables if existential quantification is introduced because you have to pack code with the data, rather than being able to inline the data from a statically known index of methods into the concrete call-sites.
As it stands, I'm more likely to want Clean-style uniqueness typing in Haskell for a non-GC'd life-cycle for memory than I am to use Rust, but it's nice to see what we can accomplish with linear types baked into the compiler. Wish regions hadn't been abandoned, but it seems like somebody wanted Rust pushed into becoming a product quickly.
> Wish regions hadn't been abandoned, but it seems like
> somebody wanted Rust pushed into becoming a product quickly.
No idea what you're referring to here. Lifetimes are entirely based on the regions literature, and four frigging years of design iteration is hardly "pushed into becoming a product quickly". :P4 years when it is a recapitulation of existing technology and hasn't had time to push anything forward and we already have PLs with similar facilities? That's a product-oriented rather than research-oriented direction whether you think it's too fast or not. For comparison, Haskell dates to the early 90s, based on non-strict FP languages from the 80s. ML itself started in the 70s. Much of what makes Haskell nice today happened because it had a decade to gestate without the demands of industry coming first. Applicative wasn't discovered until 2008. Those discoveries are a big part of why I happily use Haskell for work today.
This is emphatically not a value judgment, I will likely not use Rust in anger so I'm not your customer anyway at least WRT the programming language. Representations of linear types embeddable in dependently typed languages (such as Brady is figuring out in Idris) will probably be the next step.
In my ideal universe, there's a language for people to experiment with theoretical models and practical applications of linear typing such as Idris provides for DTPLs. This is particularly appealing as it could enable programmers to define their own linearly typed models for the compiler to enforce.
I find it profoundly disquieting when redactions are created around the history of projects, pushing the impression that the place arrived at was what was wanted all along. Our failures can inform posterity as much as our successes.[1] The core Rust team has been perfectly candid about the history of the project.
[1]: Cf. pre-Monad IO subsystems for Haskell, http://www.scs.stanford.edu/~dbg/readings/haskell-history.pd... and http://research.microsoft.com/en-us/um/people/simonpj/papers...
If what Rust has is understood to be regions, then I need a couple of words for distinguishing the two. Ordinarily, I refer to what Rust/C++ have as "ownership types" and what exists in research as regions/RBMM/RC.
[1]: http://www.researchgate.net/profile/Simon_Helsen/publication... (the defintion provided here is what I understand "regions" to mean, as contrasted with what exists in C++ and Rust)
Also, I do not know what other 'PLs with similar facilities' exist with anywhere near as much effort put into them (other than Cyclone, which is dead). You mentioned C++, but it doesn't have lifetime checking at all, which is of course a core feature of Rust...
I checked the docs and it looks like at least http://doc.rust-lang.org/0.12.0/guide-lifetimes.html#named-l... is covered which was one of my objections. I still don't like that I can't design my own linearly typed constraints, but it appears that was never a goal to begin with. Not surprising given the lack of emphasis on expressive types.
As it stands my only options for linear'ish types are indexed Monads in Haskell or building a model of linear types in a proof assistant or DTPL.
A perfect implementation can be added to the language, but getting it to work perfectly with backcompat is hard so it might be made in 2.0.
In addition to memory safety, Rust's other goal was to improve the ability of programmers to reason about low-level concurrency, motivated by the enormous pain that both the Firefox and Chrome developers are currently experiencing by trying to adapt their browsers to a multicore world. The serendipitous discovery was that the same mechanism used to guarantee memory safety will also statically guarantee that your program is free of data races.
TL;DR: Rust's goals are guaranteed memory safety, guaranteed freedom from data races, and zero runtime overhead relative to C++.
Indeed, Graydon chose the name "Rust" specifically because he wanted to avoid cutting-edge language features in favour of old, well-tested ones.
(Reasonable people could disagree whether the 1.0 language meets that goal, but it was a goal.)
There's also overhead in the sense of complexity for the programmer, which isn't really either of those two.
Well, from the programmers' perspectives there are both read-time and write-time overheads. In C++-land, the discussion about the new (to C++) 'auto' keyword is about the trade-offs between the two.
And why not mention the overhead of the programmer foregoing these "short cuts" and hand-coding it herself.
The difference is in the mindset: Most Rust programmers will hem and haw at having to use Box<Trait> until we don't have any other viable alternative (with Rust and its enums, there usually are many better ways to do it with lower cost). Java/Go libraries are completely okay with using dynamic dispatch everywhere. While writing Java code I won't be concerned about using an interface type or `Object` or whatever because zero-cost abstractions are not Java's thing. Of course, if there is a static alternative I will still prefer it, but I won't be too bothered if I just stick with `Object`.
[1] http://www.hydrocodedesign.com/2014/04/02/higher-kinded-type...
... given you have no information about runtime behavior other than the static code.
How to achieve "zero-cost abstractions" is, as usual, a design tradeoff.
Rust -- like C++ -- lets you choose in your code whether you'd like to pay for an abstraction or not. This does produce good machine code, but has two costs: 1/ the language gets more complicated and the programmer needs to be aware of what she's paying for, and 2/ you might end up paying for stuff you end up not using (for example, all button listeners might end up being the same type, and a vtable adds unnecessary cost), so that the generated code is only optimal if you have no other information about runtime behavior.
There's another way to add zero-cost abstractions: have a single simple abstraction and a JIT to figure out at runtime -- based on observed behavior -- what the optimal machine code is. This is what the JVM does. All method calls are virtual from the programmer's perspective, and no choice needs to be made ahead of time. At runtime, the JIT views the class hierarchy and usage, and decides whether a specialized, inlined version of a function is produced or whether a vtable is actually necessary (HotSpot even makes a special case when there are exactly two implementations, replacing the vtable with an `if`). If runtime behavior changes (a new type of listener is added or even new code is loaded at runtime that adds another implementation), the JIT will notice, reconsider and recompile. So in Java, virtual or even interface method calls are also zero-cost, even though you have no choice about using them; the decision on how to implement the abstraction is done by the (JIT) compiler at runtime. This, too, is a tradeoff -- a simpler language and truly optimal code taking into consideration not only static consideration but actual runtime behavior -- at the cost of a possibly significant warmup time and possible non-optimal "mistakes" by the JIT.
---
BTW, Java can be embedded in scripting languages because those languages run on the platform itself and share the runtime. Because of the JIT -- that optimizes across libraries and languages -- the interoperation is cheaper than with C. So much so, that you get the following story: As part of the work being done at Oracle on Graal, HotSpot's next-gen JIT, they've ported various scripting languages to the new JIT, among them Ruby. They've found[1] that if they interpret/JIT the C code of the native Ruby extensions they get better performance than a "plain" Ruby runtime calling into statically compiled C, because the JIT is able to optimize across the language barrier.
So why not use "interface" keyword?
Originally I resisted them on the grounds of not being necessary, but they're used all over now. Being able to supply a default implementation is extremely useful.
Rust's traits may specify just a method signature and force the implementor of the trait to implement the method, but they may specify full definitions of methods too.
"Enumerate" in English is just a fancy word for "count" -- it means to assign numbers to a bunch of things. Which is exactly what the C concept did. The Rust usage (to mean "something that can be in one of a few different states") is new, though Java has something fairly close too.
It was a poor choice, sorry. Likewise being deliberately difficult with "trait" vs. "interface" (picking Self's jargon instead of the term that literally everyone already knows from decades of OOP) didn't serve you well. Thus we have blog posts like this needing to tell us what we probably should have been able to figure out from context.
Finally, regarding "box" vs. "block". Other languages (C# is the only one that comes to mind off-hand) have used the idea of "boxing" to imply the allocation of space for and copying of pass-by-reference data. That's sort of a different notion than simple heap allocation, so it sort of gets its own jargon I guess. I didn't complain anyway. But with Rust, a "box" really is used to refer to a dynamic heap block in any context. We sort of already had a perfectly good word for that.
Pretentious jargon isn't the worst crime in the world, but I do think Rust seems needlessly complicated in the way it likes to play Shakespeare with existing concepts.
Names are hard.
> A Rust enum isn't even "quite close" to a C enum
This is incorrect. The following Rust enum compiles down to a single byte, whose variants are represented by the numbers 0, 1, and 2: enum Foo {
Zero,
One,
Two
}
You can even give them all numeric values explicitly: enum Bar {
Ten = 10,
Eighty = 80,
TwoHundred = 200
}
And you can also just tell it where to start and let it count from there: enum Qux {
Five = 5,
Six,
Seven
}
If you throw the #[repr(C)] attribute on any of these then Rust will make sure to size them as C would on your particular platform (on my machine this attribute inflates them from 8 bits to 64 bits), making them usable directly from C as well.> Likewise being deliberately difficult with "trait" vs. "interface" (picking Self's jargon instead of the term that literally everyone already knows from decades of OOP) didn't serve you well.
But "trait" is the right term. I would agree with you if Rust traits couldn't have implementations via default methods, but they do, and interfaces usually can't (except in Java 8). Interfaces strongly suggest, well, interface, as opposed to implementation; however, traits mix and match both. You can perfectly well have traits that exist only to provide "mixin"-style implementations.
In earlier versions of Rust, traits were called interfaces (and there were separate constructs to provide implementations of interfaces), but one of the design simplifications was to unify all those concepts into one: the trait.
> Finally, regarding "box" vs. "block". Other languages (C# is the only one that comes to mind off-hand) have used the idea of "boxing" to imply the allocation of space for and copying of pass-by-reference data. That's sort of a different notion than simple heap allocation, so it sort of gets its own jargon I guess. I didn't complain anyway. But with Rust, a "box" really is used to refer to a dynamic heap block in any context.
Sorry, I just have to disagree here. I don't think anything would be simpler if the keyword were "block":
let x: Block<f32> = block 3.0;
"Block" as a verb doesn't mean "allocate" in the same way that "box" does: if anything, "block" implies something related to putting threads to sleep for I/O. And as a type, "block" sounds like a code block—i.e. something like a lambda. Ruby uses "block" for this, for instance.As does, notably, Objective-C, like Ruby due to the Smalltalk heritage.
I've never heard the term "block" used to refer to heap allocations, so I don't think it would really help.
Just go back to that tutorial with your C hat on and substitute the word "union" for "enum" and I promise it will all make sense. All your intuition about C unions will cross over just fine, and the new Rust rules (they're tagged at runtime and the compiler enforces that you can only ever use fields of a runtype-checked subtype) are straightforward extensions.
Likewise the linked blog post begins, comfortingly, with "Traits are interfaces". Once you get beyond the new jargon, you find it wraps a concept which is 95% compatible with something you've been using for years.
That the Rust team seems to find no value in this kind of naming, preferring the excess precision that comes with Create-Your-Own-Name, is what I was calling "pretentious jargon" in a previous post in the thread. It's really not that bad (I mean really, they're just names), but it doesn't speak well to where the designers heads were when they invented this stuff.
Really, that's what's starting to creep my out about Rust. Just like C++ 30 years ago, it seems like Rust has caught itself up in an internal rush (among its rock-star language nerd designers) for Masterpiece Status and sort of forgotten the goal of creating a practical tool for working programmers... At some point in the near future I have to wonder if we're going to start seeing blog posts about choosing a "sane subset" of Rust with which to develop software.
> it seems like Rust has caught itself up in an internal
> rush (among its rock-star language nerd designers) for
> Masterpiece Status and sort of forgotten the goal of
> creating a practical tool for working programmers
This is complete hogwash. Just because you disagree with the chosen terminology doesn't justify attacks on the character of the Rust developers.I'm no dummy, yet Rust is just confusing as hell sometimes. And you guys frankly don't seem to care (again: note marker "seems" to indicate a personal opinion and not a "character attack"). That turns me off. It turns lots of people off. And I don't see any significant effort being made at making it an easy tool to learn and use.
Just to name a few off the top of my head:
1. Lots of focus on friendly compiler error messages, including typo correction, automatic lifetime suggestions, and error message explanations.
2. A strong worse-is-better approach in many aspects of the language design, such as preventing reference-counted cycles (we don't try to), numeric overflow (we don't try except in debug mode), typeclass decidability (it's deliberately undecidable in corner cases to avoid complex rules), prevention of deadlocks (we don't try), userland threads (we don't implement them anymore), asynchronous I/O (it's out of the domain of libstd for now), etc.
3. Blog posts like this one to introduce aspects of Rust, as well as the tutorial.
4. The Cargo package manager, as well as crates.io.
5. Naming conventions designed to fit well with C, for example choosing "enum" over "data"/"datatype" as in ML, "trait" over "class" as in Haskell (since the latter means something totally different), but modified in some cases to avoid leading programmers of C-like languages astray (for example, "interface" changing to "trait"). This naming process has taken time, but I think Rust is in a pretty good place now. There are obviously disagreements as to the naming, but we can't please everybody.
Certainly we weren't perfect, but there was a lot of effort put into making Rust as easy to use as possible.
This is blatantly false. I've been in touch with Rust development for quite some time, and pragmatism has been paramount in all design decisions. Suggesting otherwise on the basis of disagreeing with some naming choices is complete nonsense.
This is not true. Rust enums allow you to match on which of the types you have. C unions do not. If you want to implement a switch statement over the possible members of a C union, you need to put it inside a struct with a type field. You don't need to do so in Rust, and you can't do so and have it compile.
(That said, if your real complaint is that the official docs on enums are confusing, I'd certainly agree with that.)
I'd love more specifics about which docs, if you have some time.
I had to go look it up in the reference, where it is sort of hidden too.
Specifically I ended up rewriting most of the enum page: https://github.com/geofft/rust/blob/trpl-fix-enums/src/doc/t...
I think there's more that can be done (e.g. the book doesn't document that if every variant of an enum is data-less, you can cast it to an integer), but hopefully this is a start.
They're not. Payload-less enums devolve to C enums. Rust's enum simply build the enum+union enumeration pattern into the language, and allow leaving out the "union" part.
F# actually uses box much like Rust does:
// box an int
let o = box 1
Examples:
http://fsharpforfunandprofit.com/posts/cli-types/MSDN: https://msdn.microsoft.com/en-us/library/ee340516.aspx
(I agree that "enum" is a wrong name to use, but apparently for different reasons.)
They aren't really interfaces. They can be like interfaces (just method signatures) or like classes without data (including definitions for some or all methods). Using a name distinct from either "class" or "interface" limits the degree to which incorrect expectations from similar-but-critically-different constructs in other language interfere with understanding of the Rust construct.
That scares me tbh.
Sounds a bit like a Ruby monkey patch. What happens in case of conflict - my trait adds a .hash method, but there already was one?