Add support for classical OOP in Rust

I just started to getting information about Rust. As I use only OOP (inheritance, polymorphism, encapsulation, abstraction) its support is very important for me.

However, I was surprised to discover that Rust doesn't support it. It is 21 century, but Rust doesn't support it. Even JavaScript can do the following

class Person {
    constructor (gender, age) {
        this.gender = gender;
        this.age = age;
    }
}
class Child extends Person {
    constructor (name, gender, age) {
        super(gender, age);
        this.name = name;
    }
}

But Rust can't.

Is there any chance that Rust will support classical OOP? I am sure that Rust will become more powerful and popular if it supports classical OOP. So I suggest to add it.

No.

You say that as if OOP is the only way to achieve all of those four things. Rust does not have inheritance, but it still provides language features that allow you to have polymorphism, encapsulation, and abstraction.

27 Likes

On the positive side, there is some hope languages like C++ may stop supporting OOP someday :slight_smile:

17 Likes

Exactly. It's the 21st Century not the 20th Century. The world has realised that OOP is not the only way to do things and often not the best. Even Bjarne Stroustrup himself emphasises that about C++ now a days.

The class keyword was never required in JavaScript and is a new feature of questionable merits.

Perhaps not in the OOP way presented. But consider, every kind of software from operating systems to compilers to application programs was written before OOP based languages, and still is written without them, in Rust for example. I suggest that indicates such OOP is not really necessary,

5 Likes

The JavaScript example I took from this SO question - inheritance - Is it possible for one struct to extend an existing struct, keeping all the fields? - Stack Overflow

Please, note that it was viewed 90k times. At the same time, I doubt that Bjarne Stroustrup will find this code nice (from the same link):

struct Person {
    age: u8,
}

struct Child {
    person: Person,
    has_toy: bool,
}

impl Person {
    fn new(age: u8) -> Self {
        Person { age: age }
    }

    fn age(&self) -> u8 {
        self.age
    }
}

impl Child {
    fn new(age: u8, has_toy: bool) -> Self {
        Child { person: Person::new(age), has_toy: has_toy }
    }

    fn age(&self) -> u8 {
        self.person.age()
    }
}
1 Like

I grew up with OOP and learned to think about types in terms of "is a" and "has a" relationships. Like, in your example, "every child is a person".

This works quite often quite well, but it turns out upon careful reflection that things are not that simple. There exists hardly a purer example of a "is a" relationship than "every integer is a rational number", and still classical OOP fails at representing the numerical tower.

The reason for that is that OOP's "is a" relationship in fact does not match what one would naively expect.

(to be continued if there is interest.)

11 Likes

You shouldn't be. Some time after OOP was invented, people discovered that inheritance was basically a "bad idea", and it is better to use "composition" rather than "inheritance". I expect this is the reason Rust does not have it.

Edit: the reason inheritance is not a good idea... well, I guess it was discovered by experience partly, but it leads (generally) to a nasty mess.

10 Likes

That Person, Child example is a perfect example of why I never understood why everyone was so keen on OOP, especially inheritance, since I first ever heard of it with C++ and then Java.

Right there, baked into the design of the code, is the idea that only a Child might have a toy. Well, how is that model going to work out when you discover that grown adults also have toys, like myself.

In such a small example that's not a big deal, you just rewrite the whole thing. For large code bases that grow on such models through layers of inheritance this becomes unmanageable. Such classification just did not make logical sense to begin with.

Aside: I never did like the term "inheritance" in programming languages. For example I inherited properties from my parents. Yet I can still exist as an independent being without them, as I now do. That is not true of an OOP class that is derived from some base class. It forever depends on having it's parent(s) around. It is suggested, and I agree, that we should be seeking to minimise dependencies between our software components but such inheritance bakes in dependencies.

8 Likes

From the link you provided

... Ideally all reuse can be achieved by assembling existing components, but in practice inheritance is often needed to make new ones. Therefore inheritance and object composition typically work hand-in-hand, as discussed in the book Design Patterns (1994).[3]

Composition and Inheritance are two different things. When I assemble a Car using Engine, Transmission etc I use composition. When I have Button, TextButton, AnimatedButton etc I use inheritance.

When people talk about Composition over Inheritance, they are usually, specifically talking about behaviour; that is, composition of behaviour, not composition of data.

1 Like

I also thought so. I thought: A car IS a vehicle, but a car HAS an engine. But what about: a car IS a motor vehicle? So the differentiation between composition and inheritance is not as clear-cut as one might think.


A further problem is that in classical OOP both "car" and "vehicle" may be realized as concrete types at the same level of abstraction, i.e. both types can be instantiated. But that doesn't make any sense:

Let's say that in my program it makes sense to have objects of type "car" and objects of type "bicycle". But then it doesn't make sense to have objects of type "vehicle". What should such a concrete vehicle even mean? In the mental model of this program a vehicle should not be something that can be instantiated.

That's why even in classical OOP it's usually a bad idea to derive one concrete class from another. Abstract base classes are a different thing, they describe interfaces and behavior. But that we doe have in Rust: we have traits, which are like abstract base classes and C++ concepts at the same time.

1 Like

@grothesque OOP is not a silver bullet for all problems. It is clear. But in many cases, in most of cases OOP works very well. I won't even prove it.

It’s unlikely that OOP will be added to the core language, but some lower-level features that enable a crate-based OOP solution might be possible, with a sufficiently considered proposal.

You can implement the mechanics of a C++-style inheritance system with Rust generics, it just looks uglier and takes a bit more boilerplate. Given that, you should be able to write a crate that makes this style less cumbersome. If that crate becomes popular and/or demonstrates some shortcomings in Rust’s current feature set, you can use that experience to identify and support proposals for ways to improve the situation.

3 Likes

In most of these cases Rust's traits will work even better.

If in OOP one models the world in terms of nouns only (a car has an engine and is a vehicle), traits add adjectives.

For example: Rust has a Read trait which means "can be read from like a file". But that Read trait is implemented not only by files, but also network data streams, and even slices of bytes in memory and even (indirectly) by vectors of bytes. It would be difficult if not impossible to model this with classic OOP.

2 Likes

I have no doubt what you say is true. However I don't know where I would draw the line between "many" and "most".

But one could also say that some other paradigm that is not OOP, as Java or C++ see it, also works very well in many/most cases. As I said all kind of large and complex software was and is successfully written without it.

Now I am no programming language designer but I get the notion that language features cannot just be bolted on as suggestions come in. One has to have a "paradigm" or general approach to programming in mind and build features to support that. Other features may then be detrimental to the language design aims, may not work well with what one has, or may be unnecessary given other features in the language. Adding such "warts" to the language then just makes it more complex and uglier without providing much/any benefit.

Anyway, if one really wants inheritance in Rust it can be done. Consider this example of building a GUI in Saint:

slint::slint! {
    component MemoryTile inherits Rectangle {
        width: 512px;
        height: 512px;
        background: #3960D5;

        Image {
            source: @image-url("icons/bus.png");
            width: parent.width;
            height: parent.height;
        }
    }

    export component MainWindow inherits Window {
        width: 512px;
        MemoryTile {}
    }
}
...
...
    MainWindow::new().unwrap().run().unwrap();

Likely there are further possibilities for this approach.

2 Likes

Rust likes to pass objects by value (use Person rather than always Box<Person>), but this combined with subclassing leads to the object slicing bugs.

5 Likes

What you're describing specifically is implementation inheritance. OOP includes many things and this is just one of them, and it is one that Rust does not provide. There are definitely cases where implementation inheritance is useful and avoids boilerplate.

I think it is a mistake to say that implementation inheritance has no value, and also a mistake to say that Rust does not support OOP at all. It depends on which aspect of OOP you're thinking about, so it is best to be specific.

For example, Rust does fit the first paragraph in the Wikipedia description of OOP:

Object-oriented programming (OOP) is a programming paradigm based on the concept of objects,[1] which can contain data and code: data in the form of fields (often known as attributes or properties), and code in the form of procedures (often known as methods). In OOP, computer programs are designed by making them out of objects that interact with one another.[2][3]

In Rust we have types such as structs, and those types have fields and methods that encapsulate the data and code for objects of that type.

The reply from @2e71828 above is a practical answer to the question: will Rust support implementation inheritance.

3 Likes

There's a whole section in the book about this: Object Oriented Programming Features of Rust - The Rust Programming Language

I would suggest reading that carefully, and describing more the problem you're trying to solve, rather than the specific solution shape you're expecting to copy-paste from Java.

12 Likes

The problem is that you can't prove it. In fact in a typical OOP libary you couldn't actually prove anything.

The fig leaf, LSP, was already mentioned. It even looks innocent enough:
𝑆⊑𝑇→(∀𝑥:𝑇)ϕ(𝑥)→(∀𝑦:𝑆)ϕ(𝑦).
Or in textual form: Let ϕ(𝑥) be a property provable about objects x of type T. Then ϕ(𝑦) should be true for objects y of type S where S is a subtype of T.

Sounds simple. We should test ϕ for every S and T, but… stop, stop stop. What is that ϕ?

Is it ∀ϕ? No, it wouldn't work: Child definitely have some different properties that other Person doesn't have, or the whole excercise is moot.

Okay, maybe it's ∃ϕ? No, that wouldn't enough, we may need certain properties to be common between Child and Person and it's hard to predict which ones.

Then… what the heck is ϕ? The answer to that is “simple”: that's property that you may need to hold both for Child and Person. In any future program that would use Child and Person.

Thus… to make OOP work we need a crystal ball that would tell us the list of things that we may ever decide to do with Child and Person.

That's… quite a tall order.

And that's why Rust doesn't have OOP: OOP is not a math. It's clever but fragile hack which was plenty useful on small computers of XX century.

In XXI century… it's time to forget about it.

Creators of Rust really tried to bring OOP into it, but, ultimately, “Rust is an ML in a C++ tenchcoat”. Math. And math theory for OOP does't really exists.

All these discussions about whether to use Composition or Inheritance and if it would work or not are coming from one root cause: without knowing in advance whether some property is that magical ϕ that you may need some time down the road… it's impossible to say if Child have to descendant of Person or not.

Heck, that core sin is even teached in almost every OOP course when they discuss Shapes, Rectangles and Squares. Because answer to the question “is Squares as Rectangle” is quite non-trivial in OOP. Or, rather, there are no answer, it all depends on what do you plan to write.

Thus yes, it's XXI century, it's time to forget about OOP. It was nice dream to bring encapsulation, inheritance and polymorphism in one place… it's classic “pick any two of three” situation and Rust provides any two, just not all three together.

7 Likes

Thus yes, it's XXI century, it's time to forget about OOP.

I read your post with great attention. Only one question - are you serious?

Here is not magic formula but pure statistical data:

github -> language:java - > 6.6M results
github -> language:c# - > 1.9M results
github -> language:c++ -> 1.7M results

Information about other OOP languages you can find yourself. Oh. I almost forgot:

github -> language:rust -> 122k results
1 Like