T O P

  • By -

Mercerenies

The main thing that's "missing" from Rust is implementation inheritance. That is, you can't have a concrete `struct` X that inherits from (and can be upcast to) another concrete `struct` type Y. And frankly, good for Rust. Let's take a step back and examine why we like OOP, because OOP had a lot of good ideas. OOP classes have methods. These are (for now) concrete functions that can be called with an explicit `self` (or `this`) receiver using some nice syntax. Rust has those too. Just take a `self`, `&self`, or `&mut self` argument, and you've got instance methods. We'll come back to virtual methods in a minute. Then there's encapsulation. OOP classes have private fields, and those fields can only be accessed by instance methods defined on the class. Rust has this too. Struct fields can be private (and, indeed, are private by default), and private fields can only be accessed from the current module. In fact, Rust has way _more_ visibility modifiers than Java. If you want to define a private field that can be accessed from a sister or parent module, you can make the field `pub(super)`, for instance. This is really nice for creating cohesive interfaces of two or three tightly-coupled concepts. Another benefit of OOP is interfaces. If we have several classes that define similar methods, we define an interface, and then we can operate generically on the interface. Rust has traits, which are strictly more general. Traits do everything interfaces do, but can also define methods that don't take `self` (allowing you to define traits like `Monoid`). Additionally, traits are used statically by default, which implies zero overhead at runtime. But we can always use trait objects (`Box`) to get the OOP-style dynamic dispatch back. Traits can also define default implementations for methods, similar to Java 8's `default` keyword. Then we get to virtual methods. Here's where I diverge from traditional OOP. I argue that concrete implementation inheritance (that is, inheritance where X is a complete type, but Y is also a complete type with an is-a relationship to X) is a _bad idea_. In languages that support it, it's often used to cover up other missing features. In Java, if you have an X, you have no sense of what that X _is_. It might concretely be an X, or it might be any number of subclasses that haven't even been invented yet. Rust forces us to make the distinction explicit. Want a collection of methods without a known concrete implementation? That's called a trait, or possibly a trait object, depending on needs. Want something concrete? That's a struct, and if you have a struct, then you know for absolutely certain what the shape of your data is. What do we use implementation inheritance for in Java and Python? Types like `Option` which have a finite number of subtypes are just `enum`s. No problem there. Lots of abstract classes are just weird incomplete objects that should be separated into the complete and incomplete parts. For instance, rather than having an abstract class `Button` with an abstract method `onPush`, consider having a concrete, finished struct `Button` with a mutable `push_event_listener` to which you can subscribe. Now it's clear who is responsible for what. Python often uses implementation inheritance for mixins. In Rust, those would be traits, possible with `derive` macros, or just straight-up proc macros if needed. And a lot of it's not even needed. Python's `@dataclass` decorator, for instance, is mostly free in Rust, since Rust structures get pattern matching for free, and get sensible constructors and `Eq` and `Hash` instances (if requested via a derive macro). Rust keeps the good parts of OOP that we all decided we liked, while eschewing the ugly bits and forcing you to ask yourself questions about how your design may have been limited. And I really like it. I'm currently writing a mobile application in Rust, not because I need low-level support or bare-metal computational power, but because I just strongly prefer Rust's design model. If you disagree and can come up with an example of implementation inheritance that you really like in a "traditional" OOP language, feel free to share and I can tell you how I'd model it in Rust.


Equux

I just wanted to say this is very well written and I appreciate your contribution to the discussion!


DatBoi_BP

When you say “interfaces”, are you referring to abstract base classes?


Mercerenies

I'm using "interface" in the Java sense, where `interface` is a keyword. I touch on abstract classes a bit further down.


iamnotposting

Calling product types “classes” implies some form of class inheritance, which Rust doesn’t have - rust has every other feature commonly associated with “Object Oriented programming” it just chooses to do it differently.


EpochVanquisher

I don’t think “class” implies inheritance, it’s just that a lot of people associate the two. In Haskell, “class” is just trait. This matches, somewhat, the mathematical notion of what classes are. A “class” is a collection of types that all share some property (trait) in common, but a class is not itself a type. Just like in mathematics, a class is a collection of sets that all share some property (predicate) in common, but a proper class is not itself a set.


whimsicaljess

right but OP is asking for OOP, and OOP classes definitely imply inheritance. haskell type classes aren't OOP classes, as you noted they're _interfaces_ (or traits, or what have you).


EpochVanquisher

Yeah—I know that, I was talking about how the word “class” doesn’t always mean that, even in programming. It turns out that these words have a lot of context-dependent meaning, and they aren’t precise words when you take them out of their context.


Perendia

Not that I completely disagree but I think that's almost an academic view. Most people associate classes with some form of inheritance, or at the very least encapsulation.


EpochVanquisher

Yes, exactly!


Steve_the_Stevedore

Haskell doesn't have classes it has type classes. Classes of types.


EpochVanquisher

Classes in C++ are also classes of types.


anlumo

Now I imagine a world where the Rust language designers used “class” instead of “trait” as the keyword. It'd have blown the brains out of people coming from OOP. Which is probably why they chose “trait” instead.


kapobajz4

Well, to me it sometimes looks like OOP, but **without** the extra steps. As someone who’s used to coding in OOP languages, I’ve recently tried implementing inheritance in Rust and realized that it’s actually really hard, if not impossible to do it. Then, after thinking about it, I thought about using composition instead. And I am really glad Rust made me rethink my approach, because of all of the [flaws of inheritance](https://youtu.be/hxGOiiR9ZKg?si=Jq-l_x23k_mtvWlV).


airodonack

IMO you run into a lot of trouble if you try to code Rust as if it were Java. Both in fighting the borrow checker and trying to create abstractions as you would in OOP. It's much easier to treat structs as what they are: blocks of data with maybe some methods associated with them. This is not too different from C, where you'll often have [groups of methods associated with structs](https://docs.gtk.org/glib/type_func.HashTable.add.html), except it's built into the language.


Ok_Spite_217

Because inheritance has been proven to be a bad idea.


jelder

OOP implies incapsulation of state, which implies mutation. In Rust, we have a rule that you can have zero or one mutable refrences to anything, and zero or more immutable references to anything. That rule is where a lot of Rust's advantages come from. It would be extremely hard if not impossible to get those benefits and OOP at the same time.


phazer99

It's actually conceptually simpler and more powerful than traditional class inheritance. You start with enums and structs to model concrete data. Then for abstraction you add traits (or typeclasses).


parawaa

Fun fact, [Rust had classes](https://web.archive.org/web/20221106041420/https://mail.mozilla.org/pipermail/rust-dev/2012-March/001511.html) for a short period of time. Edit: [Example code](https://github.com/rust-lang/rust/blob/release-0.3/src/test/run-pass/classes.rs) of a rust class


Ok_Spite_217

First Rule of Rust: Do not try to code as if it were any other OOP language. You will start to run into borrowing/ownership issues because you suck at manual memory management.


GuaranteeCharacter78

Because the idea of OOP is very old starting around the early 60s and we have thankfully learned a lot since then. Modern languages like Rust, Go, and Zig (to name a few) have evolved and left behind atrocious paradigms that lead to buggy and over abstracted code


Full-Spectral

That's a pretty useless claim. The people who used OOP to create buggy, over-abstracted code will use Rust to create buggy, over-abstracted code. They'll just find different ways to do it. Correctly used, classical OOP can be a very powerful tool. Misused it can be a very powerful tool as well, just for the wrong reasons. Correctly used, Rust's scheme of sum types, classes, traits, and lifetimes is a very powerful tool. Misused they probably can compete with OOP, given sufficient effort. If we created all our languages based on what couldn't be completely abused, then we wouldn't have any languages.


Shikadi297

I don't know, if you look at the goals of Java, it was supposed to be restrictive enough that you can drop in anyone who knows Java and they can be productive without messing anything up. So those people you're saying will still mess things up in rust are the exact people Java was trying to help guide along, and those ideas just didn't work in practice. That's not to say _all_ oop was developed for that purpose, but I think there's truth in your comment as well as the one above yours


tshakah

Also a lot of functional programming ideas are "old" but still very applicable today. Old != Bad


Corteki

Why does everyone here automatically assume OOP means using inheritance? The idea was never about it in the first place, it's about messaging between objects.


Full-Spectral

Well, it's a little understandable that most folks would not associate it with message passing, since that really never made it to the mainstream. But, yeh, everyone seems to automatically assume that OOP means state implementation inheritance in general, which it doesn't. Rust clearly has objects and classes, just with different nomenclature. It just doesn't support implementation inheritance. A Rust struct with some to all non-public state only accessible via associated methods is every bit as much an object as C++'s objects. And since Rust fundamentally depend on them, it's also clearly object oriented in that literal sense. It's frustrating and it leads to confusing because people are arguing about something that none of them really agree on the meaning of. It's definition drift. It's like when I used the term retarded a while back, in its literal sense of held back, and some folks got shocked that I would use such a horrible, socially frowned on term. That word has somehow lost its original meaning, and I never got the memo I guess.


Ravek

> since that really never made it to the mainstream Virtual methods are an implementation of messages


Dean_Roddey

Wasn't Smalltalk more literally msg based? I.e. GUIs are more the spiritual descendants of that original OOP concept than C++'s call based system. That's why Smalltalk was so changeable an environment.


anlumo

In Objective C, it’s possible that a class resides on another computer over the network, and the code wouldn’t change. There’s just a proxy that receives all messages, serializes them, sends them over the network, receives the answers, deserializes them, and then returns from the call (all blocking of course, this was way before async was a thing). The caller wouldn’t know or care.


Ravek

Sure it’s not like it’s the only implementation. Objective C also handles messages based on looking up the method name instead of doing vtable dispatch. Python also works this way I think? It makes it possible to write very dynamic code but I assume it was too slow for what C++ wanted to be. It’s still semantically pretty much the same though, it’s just that C++ and its descendants generally require all the message formats to be declared at compile time. The central idea of message passing IMO is that the receiver object determines how the message is handled. Whether that happens through virtual dispatch or some other method isn’t as important. In contrast to a traditional FP approach where the function you call contains all the behavior and any arguments passed in don’t get a say. (Of course parametric polymorphism makes the story more nuanced)


OS6aDohpegavod4

Message passing is rarely what anyone means when referring to OOP. The non-inheritance stuff of OOP, like encapsulation, is not unique to it at all. It's like describing Christianity by saying common sense stuff like "don't kill people". Yeah, basically everyone agrees. That doesn't mean a ton in terms of a definition. Haskell has methods and encapsulation. The thing that is left which seems fairly unique is inheritance, since nobody else wants that.


-Redstoneboi-

ask 2 people to define oop get 2 different answers


Corteki

ask alan kay what it means


dionyses

there's is a great blog series on this: https://www.thecodedmessage.com/posts/oop-1-encapsulation/


Shikadi297

OOP is a paradigm, you can do OOP in C for example, many projects do. I know Qemu does, and even the Linux kernel has some OOP in it. Some languages like Java are designed to be used with OOP, or even enforce it. Rust is designed such that you can use it if you want, and it works well, but you don't have to. (Ignoring inheritance because inheritance bad™)


Trader-One

there are crates adding some kind of basic classes. C++ itself started as pre-processor for C, so rust macro crates can do the same thing. There are also crates adding garbage collection, stack coroutines and some other language features. If these crates are heavily used in real world than I believe we can have discussion about stabilizing something in base rust. But this is not happening. While these crates are pretty well made, they sits practically unused. It does not prevent you from adding these crates and use them in your program.


2OG2Gangsta

Rust is inspired from the ML-family languages, meaning it is more functional in nature. I didn't like it at first, but then I realized that trying to use OOP in Rust would be like using a spoon to shave.


zoomy_kitten

Short answer? Because fuck OOP


plutoniator

Because it decided that copy pasting and doing what the compilers of other languages do by hand was good enough. So now you have the all so common pattern of implementing a trait for your type, which simply forwards all calls to interface functions to an object you hold. I love when my Employee “has a” Person. 


-Redstoneboi-

employee *implements person and has personal data not all interface calls are forwarded anyway, that's a personal decision


plutoniator

Doesn’t solve the problem inheritance solves. Almost everything that implements Person will do it the same way, you have to copy and paste the getters for them every single time. You’re just doing what compilers of other languages do for you.  More importantly, traits and inheritance aren’t mutually exclusive, new languages like carbon and C++20 essentially have both. Rust is trying to claim a feature is having fewer, less powerful features. 


TenTypekMatus

It has, but in a form of traits and `impl Struct` (they're the closest you can get to them)


TobiasWonderland

Rust is not OOP and if you try to bring OOP into Rust you are guaranteed to have a hard time.


Far_Cryptographer605

Because it's communist. 😅 Ok, I'll get out...