Adding true OO capabilities to Rust

In languages there are many valid approaches, each with different trade-offs. Rust has equivalent of a "class" via struct Foo {} + impl Foo {}.

This clearly isn't what you're used to or what you like, but it is elegant in its own way: separates definition of data from code, and avoids having duplication of struct and class in the language that are almost the same thing (in Rust they are the same thing).

The impl syntax also combines well with generic trait bounds. This is another intentional design elegant in its own way. Instead of introducing an additional template class syntax, there's one impl syntax that adds method to a type, whether they're inherent methods or come from traits, and the methods can be available conditionally depending on generic parameters used with the type (e.g. a vector can be serializable only if its element type is serializable).

Rust has a form of multiple interface inheritance via the trait system. A single type can implement multiple traits (traits behave like Java interfaces in this case). Here's another advantage over a classic class approach: other types, even primitive types like integers, can also implement traits without having to be objects or inheriting from something specific.

21 Likes

Experience might correlate strongly with wisdom & knowledge but it does not entail it.

There's so much wrong here:

  1. C# exists because Microsoft wanted to extend Java in incompatible ways which was illegal according with Sun's terms and conditions. So no wonder it has the same OO implementation. Anyone who actually has any experience with either knows that best practices are to avoid inheritance as much as possible. "Composition over inheritance". This isn't a new concept either - the famous gang of four recommended the same in the early 90s and their design patterns in large part are in fact the results of applying said principle.

  2. Popularity of Java style OO has many reasons. Putting it all on some superiority of its OO style is oversimplified and incorrect. Java popularity has a lot to due with its promise of portability and simplicity to name just two other important factors that have nothing to do with its specific flavour of OO.

  3. Someone your age should have been more knowledgeable about the history of OO. The original idea comes from Simula and Smalltalk to name a few prominent precursors. Smalltalk for example has a very different style compared to what you're familiar with in Java. Smalltalk was very popular and it imploded unto itself due to reasons unrelated to its OO flavour.

To summarise, one specific implementation that you've been using for 40 years, isn't the only possible design nor was it the first, nor most popular. It certainly isn't the one and only design exposing "true capabilities" as stated.

More over, the attitude of "I'm not going to explain myself" is condescending and inappropriate. You came to tell us that Rust is crude and inefficient in your opinion yet you haven't bothered to understand what and why idiomatic Rust is. Haven't you been taught "When in Rome, do as the Romans do"?

15 Likes

Hi Yiga,

Thanks for your sage advice.

Someone your age should have been more knowledgeable about the history of OO. The original idea comes from Simula and Smalltalk, to name a few prominent precursors. Smalltalk for example, has a very different style compared to what you're familiar with in Java.

Actually, the Dynace extension I wrote most closely matches CLOS, the Common Lisp Object System. This is a significant superset of the Java model, and even a superset of the Smalltalk model, and is based on metaclasses.

So, in terms of sophistication, it looks like this:

  • CLOS (the most advanced)
  • Dynace
  • Smalltalk
  • Java / C#
  • C++ (the least advanced)

In terms of ease of learning and using, it looks like this:

  • Java / C# (easiest to learn and use)
  • Dynace
  • Smalltalk
  • C++
  • CLOS (most difficult to learn and use)

And in terms of speed, it looks like this:

  • C++ (fastest)
  • Dynace
  • Java / C#
  • Smalltalk
  • CLOS (slowest) (because it is so dynamic)

Dynace attempts to make a system that is easy to learn and understand (like Java), yet provides a great deal more power and flexibility (like CLOS). (Dynace was written either before Java existed or certainly before I was aware of it.)

Interestingly, when designing Dynace, I had to come up with all of the kernel class/metaclass relationships. I spent a bunch of time on it and ended up with something fairly complex. I later compared it to Smalltalk and it matched exactly. I guess I came to the same conclusions. (See InitKernel() in https://github.com/blakemcbride/Dynace/blob/master/kernel/kernel.c)

My non-expert opinion of Rust is that, in terms of speed, it is similar to C++. However, it is even less sophisticated and harder to understand than C++. I thought I might be able to help.

C# exists because Microsoft wanted to extend Java in incompatible ways which was illegal according with Sun's terms and conditions.

See Embrace, extend, and extinguish - Wikipedia

More over, the attitude of "I'm not going to explain myself" is condescending and inappropriate.

Given a lot of experience and little time, I have two choices:

  1. Stay out of it

  2. Provide what little input I can and help with pointers

I suppose #1 may make more sense. I apologize.

I've had a lot of disputs with a friend of mine about what is better OOP or non OOP (based on whether X lang supports inheritance) and what we've come up with is:

  • If one wants to model a world as a set of entities interacting with each other then we get smalltalk => actor systems.
    These themselves can (and are) implemented in rust and therefore not the right level of abstractions here.
  • If one wants to make code sharing via inheritance than it's one trade off
    If via the other (module system + functions and their composition) - it's another trade off
  • If you want both - some academic folks have you covered and have invented first class match branches - but they also have downsides

At the very end it's just about what kind of tools would fit the problem better. Sometimes, the JS is best, sometimes the Elixir, sometimes it's Rust, sometimes it's Idris.

Moreover, implementing any of these:
(the other two are provided via more general trait mechanism)

Will require a good amount of design work and will clash with what we already have.

Data structures in rust are more of C ones than C++ ones and we don't usually expect these to be objects (which is necessary to provide even a singular inheritance, not mentioning multiple and metaclasses)

2 Likes

Having used Common Lisp myself in the past, I can say that:

  • Rust may (indirectly) inherit DNA from the Lisp family, but it is decidedly no Lisp. That's not good or bad, it just is.
  • Pizza and ice cream are both great, but mixing them 0results in an inedible mess. And it's analogous for CLOS-like OO features and Rust.
  • In retrospect (and at risk of sounding like a Blub programmer), CLOS may be very advanced, but it is also pure overkill for a lot of software written. There are a lot of CLOS features that are simply never needed, and perhaps for most folks the effort of learning all about it simply doesn't weigh up against the abstractions it provides and enables.
1 Like

Being the author of a system with nearly 10,000 classes (Arahant), I can authoritatively say that the full OO model is extremely valuable in the real world. Features such as encapsulation, inheritance, and polymorphism are critical.

1 Like

In that case it sounds more like Java/C#/Common Lisp is what you want, as they provide the features you're looking for.

1 Like

In retrospect (and at risk of sounding like a Blub programmer), CLOS may be very advanced, but it is also pure overkill for a lot of software written. There are a lot of CLOS features that are simply never needed, and perhaps for most folks the effort of learning all about it simply doesn't weigh up against the abstractions it provides and enables.

Agreed. That's why I wrote Dynace to look and act as easy as Java yet have the power of CLOS when needed.

Dynace has nearly the simplicity of Java, the power of CLOS, and the speed of C++.

Sure, but the point is that it's simply too high level and complicated to force people into bothering the layout details rust forces them into. Like, you can't have an

struct A {
   field: A
}

because it's incorrect unless you insert an indirection.

Imagine what the code and your head will look like after you write 10 000 classes in that level of abstraction))))).

In that case it sounds more like Java/C#/Common Lisp is what you want, as they provide the features you're looking for.

I use Java mostly because that is the most practical tool for what I've been doing lately. After more than 20 years of experience with C, I understand well the differences between Java and Rust. Java and Rust live in utterly different worlds and are used for vastly different purposes. I just thought I might have some constructive comments about the design of Rust.

Rust has encapsulation and polymorphism. IMO the absence of inheritance is a feature, not something that it is lacking.

20 Likes

@blake1024 From what I've seen people are mostly abandoning the type of OOP that Rust is lacking (Inheritance) because it's only the right abstraction in a limited amount of cases, and programmers will force their code into that pattern when there are many simpler and easier to maintain options.

A big part of what has made the two most successful modern languages (Rust and Go) is going with the concept of composition over inheritance. I think if you gave these languages a real solid amount of attention with an open mind you'd quickly come to understand the opinions being raised. Most of the people here are deeply intimate with both composition and inheritance which is the cause of the clash of opinion.

It would be a great benefit to have a language designer diving into Rust, from your background I think you'll really love Rust if you get to know it, and then you can raise specific ideas for improvements that fit with the overall design of the language (if you have the time of course!).

Huge props to this community for the way you conduct yourselves in a discussion like this, definitely the most mature I've come across by a large margin.

12 Likes

While I truly love composition over inheritance, I think this statement is a bit too strong. I feel like using composition today is a trend, like OOP was in the past, even when it isn't good or appropriate. But this is getting off topic.

And no, I'm not voting to adding inheritance to Rust, by any way.

1 Like

Yes I should rephrase that to a large part of Go and then Rust's success with "me" was in finding code much easier to understand with composition instead of inheritance spahgetti

So I did a quick scan through the examples, and this seems to be what the TL;DR of what Dynance is:

A C-embedded dynamic language implemented with a combination of a lightweight preprocessor to C and a runtime. It uses semantically name based dispatch, called with generated C wrappers (unclear if it can dispatch to runtime generated names, I'm guessing not), with no compile time checking: all Dynance-defined types are object in source usage. Memory is either manual dispose calls or with what looks like a straightforward exact collector (haven't checked the details, I don't know how it handles external handles, if it compacts, etc.). The type system is entirely classical, in both senses, except without any static types it can be implemented by simply copying the members from the base. There's a cooperative threading system I didn't look into much, but with a garbage collector, a simple safe-point like dynamic dispatch, and a pre-processor I don't think there's going to be any surprises.

Think any embedded language host API, but there's no codegen, just gluing C objects and functions together.

Not bad, certainly more popular systems have been worse designed, but there's no surprises, and not all that much to apply to Rust here from what I can see.

The inheritance angle is roughly covered by trait delegation (whatever that is, it's basically inheritance by another name), but maybe there's an argument to add dynamic programming? That is, something like C#'s dynamic type (in intent, probably not implementation!). In any case, I think it's implementable with a crate (checks...) and of course, it is: dyner/welcome.md at main · nikomatsakis/dyner · GitHub

9 Likes

I am confused. Are you speaking of Dynance or of Rust here ? As far as I can tell delegation is still not available in rust except if you call delegation what I might call manual delegation. That is: explicitly implementing a trait by calling an implementation of the same trait on something else. It certainly works but it also requires some amount of boilerplate. Two Rfcs had successively tried to propose a syntactic sugar for automatic delegation. Each time everybody agreed that such a feature was certainly important for Rust. In particular to fill the gap with classical OOP where inheritance can sometimes provide a more straightforward form of code reuse (even if unfortunately this is achieved by mixing code reuse with the orthogonal concern of subtyping). Alas these Rfcs have both languished (the first was open in 2015) and finally been postponed by lack of time or demand for additional design exploration.

I don't know if this kind of proposal would improve @blake1024's perception of OOP support in Rust (I'm concerned that he seems to use the word polymorphism only to mean subtyping polymorphism). However I think multiple contributors have already expressed interest in it and agreed it may address some frustrations of classical OOP programmers when they try Rust.

2 Likes

Yes, that exactly. No matter how it's implemented, if it's implemented, it's inheritance in the style of Rust, so OP should be satisfied by it.

@simonbuchan thanks for the pleasant overview. I only have this to add. Dynace supports native threads too.