There's one thing that bugs me about Rust (and also Go) and that is the lack of OO style polymorphism. I know, I know OO/classes bad, composition good. But, there are just some problems that just lend themselves really well to polymorphism in the form of having a subtype implement a method or override an implementation of a method. Is it just me? Am I missing some better way of handling those situations where this kind of polymorphism is such a natural fit?
I used to think like that, but honestly, why do you want to use something that you know WILL hurt you, just because it feels convenient?
Instead of wanting oop polymorphism, use Enums, traits, generics, etc. You will have all the benefits without all the cons.
One way that can sometimes work well for that is turning it inside out into a "strategy pattern" or "dependency injection" kind of thing. Basically, anything virtual
you make a trait method, then you have a struct
for the non-virtual stuff that calls the trait for those extension points. This is particularly effective for the equivalent of OOP protected virtual
.
Something you could do is abuse the Deref
and DerefMut
traits: Rust Playground. Since their behavior is pretty much allowing one struct to behave as another struct.
the playground above simulates some kind of OOP inheritance by embedding what would be the "super class" (which here is a struct), in what would be the "child classes" (which here are also just structs). then we implement both Deref
and DerefMut
in the child structs, with Target
being the super struct
now we can use either impl Deref
or dyn Deref
which will accept any of the child structs.
note that this doesn't allow for inheritance chains: say you want a new struct Healer
to be a child of Wizard
. then Knight::attack
won't accept Healer
instances.
@maxjeffos It's not just you. A lot of people have asked for this feature. Here's some proposals that were written back in 2014:
And here's a short summary I wrote in 2021 about the problems that come up in discussions on how to accomplish it, not from a perspective on whether OO is "good" or not (which is very subjective and problem-specific), but on whether it'll work while preserving what makes Rust good:
I think the biggest reason why Rust doesn’t have inheritance is because nobody seems to have figured out a way to implement it without requiring all subclassable types to be used through pointers. In order for a function to accept a subclass, it would have to take a
&Animal
or aBox<Animal>
. A function that acceptsAnimal
cannot accept a subclass, because Rust needs to know how big things are to pass them around directly, and a subclass may be bigger than its base. A function that accepts aBox<T>
might still have problems, because it would need to add aT: ?Sized
bound.So, since base classes are Dynamically Sized Types, how are they going to be constructed? Normally, Rust handles this by having you start with an “owned version”, like a
&Vec<u8>
, and turn it into the borrow-only DST, like a&[u8]
. But you’re supposed to be able to construct base classes directly, as long as they aren’tabstract
, like how TableView is supposed to be usable, but also subclassable. The only way you can currently construct a struct in Rust is on the stack, usingTableView {}
syntax, and this cannot work if it is dynamically sized. Effectively, this means that subclassing in Rust is dependent on placement construction , which has been postponed behind other features that were considered more important and more doable.This means that subclassing cannot Just Work. Other languages solve these problems with implicit boxing, and since Rust’s whole reason for existing is to not have implicit boxing, that’s unacceptable. It doesn’t matter whether people find subclassing intuitive or not — adding it to Rust without adding implicit boxing would make it unintuitive.
That has nothing to do with polymorphism, but with inheritance, isn’t it?
UPD: Forgot inheritance actually implies dynamic dispatch of it’s not just delegation.
IME, the underlying problem is that run time polymorphism is not a first class citizen in Rust. The only construct we have here are trait object, and they are rather clunky to use which heavily disincentivises them. This is an unfortunate side effect of the "don't hide the cost of your abstractions" paradigm.
I think the folks working on dyn*
are onto something there, you should watch out that space. Because I'm convinced that many of your polymorphism needs that are solved by inheritance in OO could equally well be solved with trait objects instead. It's just that they aren't very pleasant to use, currently.
FWIW, I know there's some lang interest in trying to make trait objects more pleasant. I don't think there's any concrete proposals on the table to do so, though.
(If you have any suggestions, IRLO threads are welcome!)
I have been tempted to do that as well in the past, but I remember people told me it was a bad idea. And when I used it, I did also run into problems later.
You already mentioned the inheritance chains that don't work. Another problem is that Deref<Target = T>
is not implemented for T
itself. So when you write generic functions or methods that accept a type that implements Deref
, this will always involve one level of dereferencing (and not zero and not more than one).
The only way where zero, one, or more levels automatically work is during method resolution. But that's not sufficient to cover all cases where you'd like some sort of inheritance.
I believe the right thing is to use in Rust is trait
s. When you need inheritance, I guess you need to use boilerplate, macros, or some other mechanisms to delegate implementations.
I have been thinking about this, but never used it except for playing:
pub mod m {
// Fields of base type
pub struct Base {
name: String,
}
// Method implementations for base type
impl Base {
fn foo(&self) {
println!("Foo {}", self.name);
}
fn bar(&self) {
println!("Bar {}", self.name)
}
}
// Public trait interface for base type and derived types
pub trait BaseTrait {
fn foo(&self);
fn bar(&self);
}
// If we don't want to allow raw access to inner `Base`,
// we can make this module private:
mod private {
use super::*;
pub trait HasBase {
fn base(&self) -> &Base;
fn base_mut(&mut self) -> &mut Base;
fn foo(&self) {
self.base().foo()
}
fn bar(&self) {
self.base().bar()
}
}
}
use private::*;
// We automatically implement `BaseTrait` for all types that implement `HasBase`:
impl<T: ?Sized + HasBase> BaseTrait for T {
fn foo(&self) {
HasBase::foo(self)
}
fn bar(&self) {
HasBase::bar(self)
}
}
// Struct `Base` has a `Base`:
impl HasBase for Base {
fn base(&self) -> &Base {
self
}
fn base_mut(&mut self) -> &mut Base {
self
}
}
// Derived type:
pub struct Extended {
base: Base,
}
// Constructor for derived type:
impl Extended {
pub fn new() -> Self {
Extended {
base: Base {
name: "Basis".to_string(),
},
}
}
}
// Inherit from `Base`, but override method `bar`:
impl HasBase for Extended {
fn base(&self) -> &Base {
&self.base
}
fn base_mut(&mut self) -> &mut Base {
&mut self.base
}
fn bar(&self) {
println!("Overridden");
}
}
}
use m::*;
fn process<T: BaseTrait>(arg: T) {
arg.foo();
arg.bar();
}
fn main() {
let x = Extended::new();
process(x);
}
Output:
Foo Basis
Overridden
Details may vary (sometimes you might not need a private submodule if it's okay to expose the Base
struct).
However, it might be overall easier to forward method implementations manually (or use some crates which provide macros for that). So I didn't really use that approach above.
With Trait objects you can get polymorphic/dynamic dispatch.
You can also provide default implementations in traits and then override them.
You can also create subtraits that extend other traits.
So, what more do you want?
I think one difference is that Rust's traits don't inherit data but only provide methods (as well as associated functions and types). As a subtrait you cannot access any private items of the supertrait (including private data) because all trait items are public.
Good point, but traits themselves aren't always exported though so I think you can accomplish any sort of visibility scheme you want with a bit more effort and splitting traits.
It certainly wouldn't be as ergonomic as say the "protected" keyword in Java.
As a subtrait you cannot access any private items of the supertrait (including private data) because all trait items are public.
This depends if you are implementing the subtrait for the concrete type vs the super trait.
For the former, you can definitely access private fields.
We have to declare them pub
though if they appear in bounds:
pub mod fails {
trait T1 {}
pub trait T2: T1 {} // we can't do that!
}
pub mod works {
mod hidden {
pub trait T1 {}
}
use hidden::*;
pub trait T2: T1 {} // but we can do this
}
Errors:
Compiling playground v0.0.1 (/playground)
error[E0445]: private trait `fails::T1` in public interface
--> src/lib.rs:3:5
|
2 | trait T1 {}
| -------- `fails::T1` declared as private
3 | pub trait T2: T1 {} // we can't do that!
| ^^^^^^^^^^^^^^^^ can't leak private trait
For more information about this error, try `rustc --explain E0445`.
error: could not compile `playground` due to previous error
As you can see in the example, the "solution" is to use a private module where the trait we want to hide is pub
. That's what I also did in this post, but ergonomics is very bad. Sometimes it results in a cascade of types you suddenly have to declare pub
and move to a private module.
But if I want to take advantage of common properties ("traits") and build some sort of inheritance chain, I will have to make implementations based on super traits.
This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.