I'm going to push back against this terminology for the sake of people who don't know Rust and are coming from traditional OO languages.
Rust traits don't "inherit" from other traits, they "require" other traits.
So if I have two traits, where one requires the other:
trait Foo {}
trait Bar: Foo {}
That doesn't add Foo's methods to Bar's list of methods, like you might expect from the term "inheritance".
Instead, it just means that it's not possible to implement Bar for a type unless Foo is also separately implemented for that type.
Despite this it's still accurate to say that this enables a supertype/subtype relationship in certain contexts, but be careful because Rust isn't a traditionally OO language, so that may not always be the most useful framing.
> That doesn't add Foo's methods to Bar's list of methods, like you might expect from the term "inheritance".
This is not completly correct. It's true you won't e.g. be able to implement `Foo` and specify `Bar`'s method in it, however it does mean that e.g. `dyn Foo` will support `Bar`'s methods.
> Despite this it's still accurate to say that this enables a supertype/subtype relationship in certain contexts
It does enable something that looks like subtyping because you're allowed to use something that implements `Foo` when something that implements `Bar` is expected. However this is not called subtyping in Rust terms; subtyping in Rust exists but is unrelated to traits (it's only affected by lifetimes)
In Rust, an implementation of a trait never implements methods that weren't defined on that trait explicitly. In your example, an implementation of C inherits from B and A, but only implements B's methods. In Rust, an implementation of C cannot itself implement B's or A's methods; it is merely allowed to assume that they have been implemented elsewhere. This also means that subtraits cannot override the behavior of methods defined on supertraits.
It might be better to think of it this way: types in Rust do not implement traits, traits are implemented for types. It might seem subtle, but its not: a trait can be implemented for a type without that type's knowledge (obviously taking care for the orphan rule). Traits are also implemented via pattern matching (Into being implemented for all Froms is the most trivial example). Go's interfaces come closer to Rust traits than those from OOP languages.
I've experienced a lot of fear in other people, especially when interviewing, about Rust not being OOP. Very smart people seem to be coming up with carve-outs to keep that religion alive. Try letting go of OOP for a month or two, and you'll find that you're better off letting OOP be someone else's problem.
The terminology that Rust uses is that "Foo" is a supertrait of "Bar". I understand that the docs so not call it inheritance, but from my experience at least people call this "trait inheritance" quite commonly.
What's the practical distinction here? I agree with you that rust isn't OOP, but for the sake of communication and understanding--what's the practical difference between requiring a trait and inheriting from it?
It means you can't just write `impl Bar for MyType` and get Foo pulled in automatically. You have to write both `impl`s yourself.
The inheritance-like syntax is shorthand for `trait Bar where Self: Foo`, and bounds like this can show up in lots of other places, where they follow the same rules: `MyType` has to impl all of them, and the traits and bounds can have fairly arbitrary (or at least un-tree-like) relationships.
The upcasting thing is a rare exception to the idea that this `Self: Foo` bound is just like any other bound.
For one, each trait has a separate namespace for its items, so in particular Foo::quux and Bar::quux are distinct even when one trait "inherits" from the other.
> people who don't know Rust and are coming from traditional OO languages.
Should those people be learning Any in the first place initially as an introduction to the language? How similar to `unsafe` is `Any` in terms of "try to avoid it"?
If you're new to Rust, you have effectively no reason to take time out to learn about the std::any::Any type. I wouldn't say it's something to "avoid"; this is unlike unsafe, where I would advise a new programmer to actively avoid it (unless they have a C background, maybe). It's just something you don't really need, and dyn traits in Rust are still relatively verbose and restrictive enough that it naturally steers you toward more idiomatic, non-dynamic code (which isn't to say that dyn traits are useless, they have their place, but it's a niche IMO).
The important change here appears to be that the internal representation of a vtable is now guaranteed to have its supertraits laid out in some predictable prefix form to its own methods.
EDIT: or if this is not possible, a pointer to the appropriate vtable is included. I assume this must be for diamond trait inheritance.
> the internal representation of a vtable is now guaranteed to have its supertraits laid out in some predictable prefix form to its own methods.
Importantly, note that the specifics of the vtable layout are not guaranteed, only the general property that the layout must be amenable to supporting this.
IIRC vtable happens to already be laid out the required way (barring some corner cases maybe, correct me if I am wrong), this RFC just made that official.
And for diamond patterns vtable entries are duplicated.
This example shows how it works for one trait, Debug, but what if you have a type that (might) implement multiple traits A, B, and/or C? It isn't clear to me if that is possible, unless the type implements all of those traits. What I'd like to do is have some base trait object and query it to see if it supports other interfaces, but also not have to have stub "I don't actually implement this" trait impls for the unsupported ones. A bit like how I might use dynamic_cast in c++.
(I believe I understand that in rust this has not historically been possible because rust doesn't have inheritance, so, there can be only one vtable for a type, rather than an chain like you might have in c++.)
> This example shows how it works for one trait, Debug, but what if you have a type that (might) implement multiple traits A, B, and/or C? It isn't clear to me if that is possible, unless the type implements all of those traits.
Yeah, you can do the same trick if you care about types that implement all of A, B and C.
> What I'd like to do is have some base trait object and query it to see if it supports other interfaces, but also not have to have stub "I don't actually implement this" trait impls for the unsupported ones.
Currently this is not possible, though it might be possible in the future once Rust gets specialization. With that you would basically be able to write the "I don't actually implement this" stub automatically. The catch however would be that this can only work for `'static` types, since such specialization for non-`'static` types is unsound.
> I believe I understand that in rust this has not historically been possible because rust doesn't have inheritance, so, there can be only one vtable for a type, rather than an chain like you might have in c++.
The issue is kinda the opposite. In Rust types can implement any number of traits (even infinite!), so they would require an potentially an infinite amount of vtables if implemented like in C++, which is just not possible. So instead this is splitted from types and moved to trait objects, which allow to carry a vtable for one specific trait.
I think you’ve just confused how traits are used. They’re more like Java interfaces and there’s no inheritance - if you have trait A: B it means whatever type implements a also separately and explicitly has to implement B. Multiple traits would work similarly - either the downcast would work if the type implements a trait or you’d get back a None when you try to downcast.
With a bit of API massaging, this could be improved quite a bit in terms of ergonomics. The challenge is that traits often require concrete structs if you want them to participate in dynamic typing.
Basically you can now make something like `Arc<dyn QueryInterface>` work, where multiple traits are available through a `query_interface`:
fn main() {
let x = Arc::new(X);
let y = Arc::new(Y);
let any = [x as Arc<dyn QueryInterface>, y as _];
for any in any {
if let Some(a) = any.query_interface::<dyn A>() {
a.a();
}
if let Some(b) = any.query_interface::<dyn B>() {
b.b();
}
}
}
the article isn't very good for anyone not already familiar with the problem
> What I'd like to do is have some base trait object and query it to see if it supports other interfaces
> rust doesn't have inheritance
rust traits and lifetimes have inheritance, kinda, (through rust types do not)
E.g. `trait A: Debug + Any` means anything implementing A also implements Debug and Any. This is a form of interference, but different to C++ inheritance.
This is why we speaking about upcasts when casting `&dyn A as &dyn Debug` and downcast when trying to turn a `&dyn A` into a specific type.
But because this inheritance only is about traits and the only runtime type identifying information rust has is the `Any::type_id()` the only dynamic cast related querying you can do is downcasting. (Upcasting is static and possible due to changes to the vtable which allow you to turn a `&dyn A` vtable into a `dyn Any`/`dyn Debug` vtable (in the example below).
The part of bout downcast form the article you can ignore, it's some nice convenient thing implicitly enabled by the upcasting feature due to how `downcast_ref` works.
So I think what you want might not be supported. It also is very hard to make it work in rust as you would conceptually need a metadata table for each type with pointer to a `dyn` vtable for each object safe trait the type implements and then
link it in every vtable. The issue is concrete rust traits can
be unbound e.g. a type `C` might implement `T<bool>` and `T<T<bool>>` and `T<T<T<bool>>>` up to infinity :=) So as long as you don't have very clever pruning optimizations that isn't very practical and in general adds a hidden runtime cost in ways rust doesn't like. Like e.g. if we look at `dyn T` it e.g. also only contains runtime type information for up casting if `T: SomeTrait` and downcasting if `T: Any` and the reason up casting took like 5+ years is because there are many subtle overhead trait offs between various vtable representations for `C: A+B` which might also affect future features etc.
Just having features associated with OOP isn't a bad thing. Object upcasting has its uses.
It's some of the other stuff that gets OOP its bad rap.
Garbage collection, common in many OOP languages, enables having arbitrary object graphs. The programmer doesn't need to think hard about who has a reference to who, and how long these references live. This leads to performance-defeating pointer chasing, and fuzzy, not-well-thought-of object lifetimes, which in turn lead to accidental memory leaks.
Additionally, references in major languages are unconditionally mutable. Having long-lived mutable references from arbitrary places makes it hard to track object states. It makes easier to end up in unexpected states, and accidentally violate invariants from subroutine code, which means bugs.
Also, class-like functionality conflates data-layout, privacy/encapsulation, API, namespacing and method dispatch into a single thing. Untangling that knot and having control over these features separately might make a better design.
You make some really good criticisms of OOP language design. I take issue with the part about garbage collecting, as I don't think your points apply well to tracing garbage collectors. In practice, the only way to have "memory leaks" is to keep strong references to objects that aren't being used, which is a form of logic bug that can happen in just about any language. Also good API design can largely alleviate handling of external resources with clear lifetimes (files, db connections, etc), and almost any decent OOP languages will have a concept of finalizers to ensure that the resources aren't leaked.
Are we talking about functional OOP or class-less OOP or JavaScript's prototypal version of inheritance in OOP or Javas functions-are-evil OOP or some other version of OOP?
Object-Oriented Programming is a spectrum with varying degrees of abuse.
OOP is a good thing, not a bad thing. It enables you to use objects to represent relationships easily in a way that other paradigms don't. I agree that the trendiness of OOP was stupid (it isn't the right approach to every situation and it was never going to solve all our problems), but the way it's trendy to hate on OOP now is equally stupid. It's a good tool that sometimes makes sense and sometimes doesn't, not a savior or a devil.
“Inheritance” of interfaces good *. Inheritance of stateful objects bad - composition is much better. The OOP model that Rust supports only supports the good kind of OOP and doesn’t support the bad kind.
* technically rust doesn’t have interface inheritance but you can treat it that way and it’ll mostly look like that in an abstract sense.
I see it has criticising rust choice to not do OOP at the beginning to finally do it piece by price and that it would have probably be better for the language to embrace it from start.
I'm a bit confused by both this comment and the previous one. Fundamentally nothing new is unlocked by this, that wasn't already possible for many years. It's just the ergonomics that got much better through this change.
There's this dynamic in the industry in which a brash young project comes out swinging against some established technique and commits early to its opposite. Then, as the project matures, its authors begin to understand why the old ways were the way they were and slowly walk back their early stridency --- usually without admitting they're doing it.
Consider Linux. Time was, metaprogramming was BAD, C was all you needed, we didn't need dynamic CPU schedulers, we didn't need multiple LSMs, and we sure as hell didn't need ABI stability. Now we have forms of all of these things (for the last, see CO-RE), because as it turns out, they're actually good.
In Python, well, turns out multiprocessing isn't all you need, and a free-threaded Python has transitioned from impossible and unwanted to exciting and imminent.
In transfer encodings, "front end" people thought that JSON was all you needed. Schemas? Binary encodings? Versioning? References? All for those loser XML weenies. Well, who's the weenie now?
And in Rust? Well, let's see. Turns out monomorphization isn't all you need. Turns out that it is, in fact, occasionally useful to unify an object and its behavior in a runtime-polymorphic way. I expect yeet_expr to go through eventually too. Others are trying to stabilize (i.e. ossify) the notionally unstable ABI, just like they did to poor C++, which is now stuck with runtime pessimization because somebody is too lazy to recompile a binary from 2010.
As surely as water will wet and fire will burn, the gods of Java with stupidity and complexity return.
> And in Rust? Well, let's see. Turns out monomorphization isn't all you need. Turns out that it is, in fact, occasionally useful to unify an object and its behavior in a runtime-polymorphic way. I expect yeet_expr to go through eventually too. Others are trying to stabilize (i.e. ossify) the notionally unstable ABI, just like they did to poor C++, which is now stuck with runtime pessimization because somebody is too lazy to recompile a binary from 2010.
Not to make an argument either way on your general point, but these are really bad examples for Rust if you look at the specifics:
Monomorphization was never the only option. Trait objects and vtables predate the RFC process itself. Early Rust wanted more dynamic dispatch than it eventually wound up with.
All the various initiatives related to stable ABI are focused on opt-in mechanisms that work like `#[repr(C)]` and `extern "C"`.
The only way to interpret these as examples of "brash young project walks back its early stridency as it ages" is if you ignore the project's actual reasoning and design choices in favor of the popular lowest-common-denominator Reddit-comment-level understanding of those choices.
> Turns out monomorphization isn't all you need. Turns out that it is, in fact, occasionally useful to unify an object and its behavior in a runtime-polymorphic way.
This actually gets the history backwards. Ancient Rust tried to do generics in a fully polymorphic way using intensional type analysis, like Swift does. We switched to monomorphization reluctantly because of the code bloat, complexity of implementation, and performance problems with intensional type analysis. "dyn Trait" was always intended to be an alternative that code could opt into for runtime polymorphism.
I'm going to push back against this terminology for the sake of people who don't know Rust and are coming from traditional OO languages.
Rust traits don't "inherit" from other traits, they "require" other traits.
So if I have two traits, where one requires the other:
That doesn't add Foo's methods to Bar's list of methods, like you might expect from the term "inheritance".Instead, it just means that it's not possible to implement Bar for a type unless Foo is also separately implemented for that type.
Despite this it's still accurate to say that this enables a supertype/subtype relationship in certain contexts, but be careful because Rust isn't a traditionally OO language, so that may not always be the most useful framing.
This is not completly correct. It's true you won't e.g. be able to implement `Foo` and specify `Bar`'s method in it, however it does mean that e.g. `dyn Foo` will support `Bar`'s methods.
> Despite this it's still accurate to say that this enables a supertype/subtype relationship in certain contexts
It does enable something that looks like subtyping because you're allowed to use something that implements `Foo` when something that implements `Bar` is expected. However this is not called subtyping in Rust terms; subtyping in Rust exists but is unrelated to traits (it's only affected by lifetimes)
In an interface/class hierarchy A < B < C, C can be an abstract class that only implements B’s methods and not A’s.
I've experienced a lot of fear in other people, especially when interviewing, about Rust not being OOP. Very smart people seem to be coming up with carve-outs to keep that religion alive. Try letting go of OOP for a month or two, and you'll find that you're better off letting OOP be someone else's problem.
Remember, when people talk about "OOP" what they're actually talking about is Java EE 6.
The inheritance-like syntax is shorthand for `trait Bar where Self: Foo`, and bounds like this can show up in lots of other places, where they follow the same rules: `MyType` has to impl all of them, and the traits and bounds can have fairly arbitrary (or at least un-tree-like) relationships.
The upcasting thing is a rare exception to the idea that this `Self: Foo` bound is just like any other bound.
But Bar requiring Foo means that if you want to use Gum in a place that expects Bar, Gum must have both Bar's methods and Foo's methods.
In some cases, you might be able to derive some of those.
I have easily translanted the raytracing in one weekend from its OOP design in C++, into a similar OOP design in Rust.
Should those people be learning Any in the first place initially as an introduction to the language? How similar to `unsafe` is `Any` in terms of "try to avoid it"?
EDIT: or if this is not possible, a pointer to the appropriate vtable is included. I assume this must be for diamond trait inheritance.
Importantly, note that the specifics of the vtable layout are not guaranteed, only the general property that the layout must be amenable to supporting this.
And for diamond patterns vtable entries are duplicated.
(I believe I understand that in rust this has not historically been possible because rust doesn't have inheritance, so, there can be only one vtable for a type, rather than an chain like you might have in c++.)
Yeah, you can do the same trick if you care about types that implement all of A, B and C.
> What I'd like to do is have some base trait object and query it to see if it supports other interfaces, but also not have to have stub "I don't actually implement this" trait impls for the unsupported ones.
Currently this is not possible, though it might be possible in the future once Rust gets specialization. With that you would basically be able to write the "I don't actually implement this" stub automatically. The catch however would be that this can only work for `'static` types, since such specialization for non-`'static` types is unsound.
> I believe I understand that in rust this has not historically been possible because rust doesn't have inheritance, so, there can be only one vtable for a type, rather than an chain like you might have in c++.
The issue is kinda the opposite. In Rust types can implement any number of traits (even infinite!), so they would require an potentially an infinite amount of vtables if implemented like in C++, which is just not possible. So instead this is splitted from types and moved to trait objects, which allow to carry a vtable for one specific trait.
https://play.rust-lang.org/?version=beta&mode=debug&edition=... (ninja edited a few times with some improvements)
With a bit of API massaging, this could be improved quite a bit in terms of ergonomics. The challenge is that traits often require concrete structs if you want them to participate in dynamic typing.
Basically you can now make something like `Arc<dyn QueryInterface>` work, where multiple traits are available through a `query_interface`:
> What I'd like to do is have some base trait object and query it to see if it supports other interfaces
> rust doesn't have inheritance
rust traits and lifetimes have inheritance, kinda, (through rust types do not)
E.g. `trait A: Debug + Any` means anything implementing A also implements Debug and Any. This is a form of interference, but different to C++ inheritance.
This is why we speaking about upcasts when casting `&dyn A as &dyn Debug` and downcast when trying to turn a `&dyn A` into a specific type.
But because this inheritance only is about traits and the only runtime type identifying information rust has is the `Any::type_id()` the only dynamic cast related querying you can do is downcasting. (Upcasting is static and possible due to changes to the vtable which allow you to turn a `&dyn A` vtable into a `dyn Any`/`dyn Debug` vtable (in the example below).
The part of bout downcast form the article you can ignore, it's some nice convenient thing implicitly enabled by the upcasting feature due to how `downcast_ref` works.
This https://play.rust-lang.org/?version=beta&mode=debug&edition=... might help.
So I think what you want might not be supported. It also is very hard to make it work in rust as you would conceptually need a metadata table for each type with pointer to a `dyn` vtable for each object safe trait the type implements and then link it in every vtable. The issue is concrete rust traits can be unbound e.g. a type `C` might implement `T<bool>` and `T<T<bool>>` and `T<T<T<bool>>>` up to infinity :=) So as long as you don't have very clever pruning optimizations that isn't very practical and in general adds a hidden runtime cost in ways rust doesn't like. Like e.g. if we look at `dyn T` it e.g. also only contains runtime type information for up casting if `T: SomeTrait` and downcasting if `T: Any` and the reason up casting took like 5+ years is because there are many subtle overhead trait offs between various vtable representations for `C: A+B` which might also affect future features etc.
It begins:
> Let's say you want to have a wrapper around a boxed any that you can debug.
It's some of the other stuff that gets OOP its bad rap.
Garbage collection, common in many OOP languages, enables having arbitrary object graphs. The programmer doesn't need to think hard about who has a reference to who, and how long these references live. This leads to performance-defeating pointer chasing, and fuzzy, not-well-thought-of object lifetimes, which in turn lead to accidental memory leaks.
Additionally, references in major languages are unconditionally mutable. Having long-lived mutable references from arbitrary places makes it hard to track object states. It makes easier to end up in unexpected states, and accidentally violate invariants from subroutine code, which means bugs.
Also, class-like functionality conflates data-layout, privacy/encapsulation, API, namespacing and method dispatch into a single thing. Untangling that knot and having control over these features separately might make a better design.
If it helps you ship the business logic, sometimes it’s okay to concede some performance or other cost.
I would argue this is correlation, not causation. And of the many flaws one can raise with OOP, that GC is pretty low on that list.
Object-Oriented Programming is a spectrum with varying degrees of abuse.
* technically rust doesn’t have interface inheritance but you can treat it that way and it’ll mostly look like that in an abstract sense.
I see it has criticising rust choice to not do OOP at the beginning to finally do it piece by price and that it would have probably be better for the language to embrace it from start.
Consider Linux. Time was, metaprogramming was BAD, C was all you needed, we didn't need dynamic CPU schedulers, we didn't need multiple LSMs, and we sure as hell didn't need ABI stability. Now we have forms of all of these things (for the last, see CO-RE), because as it turns out, they're actually good.
In Python, well, turns out multiprocessing isn't all you need, and a free-threaded Python has transitioned from impossible and unwanted to exciting and imminent.
In transfer encodings, "front end" people thought that JSON was all you needed. Schemas? Binary encodings? Versioning? References? All for those loser XML weenies. Well, who's the weenie now?
And in Rust? Well, let's see. Turns out monomorphization isn't all you need. Turns out that it is, in fact, occasionally useful to unify an object and its behavior in a runtime-polymorphic way. I expect yeet_expr to go through eventually too. Others are trying to stabilize (i.e. ossify) the notionally unstable ABI, just like they did to poor C++, which is now stuck with runtime pessimization because somebody is too lazy to recompile a binary from 2010.
As surely as water will wet and fire will burn, the gods of Java with stupidity and complexity return.
Not to make an argument either way on your general point, but these are really bad examples for Rust if you look at the specifics:
Monomorphization was never the only option. Trait objects and vtables predate the RFC process itself. Early Rust wanted more dynamic dispatch than it eventually wound up with.
The idea of a "throw"-like operator was introduced at the same time as the `?` operator and `try` blocks: https://rust-lang.github.io/rfcs/0243-trait-based-exception-... (Okay, technically `?` was proposed one month previously.)
All the various initiatives related to stable ABI are focused on opt-in mechanisms that work like `#[repr(C)]` and `extern "C"`.
The only way to interpret these as examples of "brash young project walks back its early stridency as it ages" is if you ignore the project's actual reasoning and design choices in favor of the popular lowest-common-denominator Reddit-comment-level understanding of those choices.
This actually gets the history backwards. Ancient Rust tried to do generics in a fully polymorphic way using intensional type analysis, like Swift does. We switched to monomorphization reluctantly because of the code bloat, complexity of implementation, and performance problems with intensional type analysis. "dyn Trait" was always intended to be an alternative that code could opt into for runtime polymorphism.