Suggestion for making Rust's trait system more usable for generics

moved from: Rust 2020: Growth

I think Rust can still provide better support for OOP, which is widely adopted by the industry and even taught specifically in Universities. Most people, especially new Programmers, expect features such as inheritance, because it is the most direct way of extending a given public type.

In Rust you do not find any support for this, you have to store a member of the type you want to "inherit" from, and maybe, if the foreign library provides some convenient traits you can use, you can implement those and use your derived type with the library's generic functions directly, otherwise you can only use the Into<ForeignType> for YourType trait implementation. In any case, Rust makes it easy to write very narrow interfaces, because traits are an entirely separate concept to structs/types. Rust focusses on the implementation details, which is a great power to have, but the target should always be to simplify as much as possible, in a way that doesn't obstruct fine grained control.

God forbid!

The writing is on the walls. A ghastly vision of the future. Rust will become widely used for all the compelling reasons we know and love.

Hoards of Java and C# developers will find themselves having to use Rust as companies adopt it.

They will whine and complain that it does not have OOP as they know it.

With their influence on proceedings Rust will have a "class" keyword bolted on, and all the baroque baggage that goes with that. As happened to Javascript when it took off as node.js.

And there goes the neighborhood.

12 Likes

Ugh, please don't start with this idiological hard headedness. There are reasons why other languages are being used. There are tradeoffs as well, and I am not saying Rust should forget it's roots in low level systems development. However if Rust is meant to become a modern language with a future, it does have to think about how it can utilize ideas from the "top-down" OOP world.

I have programmed in quite a few languages by now, in order, C++, Haskell, Rust, Java, Go and here and there I looked at some Javascript and python. All languages are basically just different ways of writing algorithms, using data structures. They all have different approaches to making this as simple and efficient as possible, with different priorities. Some languages leave a lot of control to the programmer, because many simplifications are inefficient. Other languages are okay with being inefficient, and prioritize ergonomics.

Rust is definitely coming from the bottom-up, low-level, control > ergonomics side of the spectrum, and that is the right way to be, because it's efficient. However it can be unproductive, which is also where a lot of bad reputation for such languages comes from in the industry. It is unproductive when top performance is not one of your priorities, which is almost always the case for new projects, except for games.

The top-down languages come from the human's perspective, not from the computer's one. After all, a programming language is a human-computer interface. Any language which is supposed to be adopted by a lot of programmers, also needs to be accessible for humans. I'm not talking about ecosystems or documentations, but actual language grammar and syntax. Rust does a great job at this mostly, especially considering that it is as performant as C++. I am just saying, Rust could strive to become even more ergonomic, especially at the object-relational level, and it would turn out a little more similar to C# or Java, and it would be more appealing to many not-so-technical programmers.

Those languages give humans tools for describing concepts and ideas, not computer programs. People think of "things with shapes and abilities". You want to be able to define classes of objects, types of objects, however you may call them, and also classes of classes, or types of types, basically non-terminal type definitions. Think Java abstract classes or C++ template specializations. Types are nothing else but the set of all objects/data structures with this specific shape, and humans want to be able to easily narrow down or extend those sets, because the real world is complex like that. You want to write functions which can take anything with only very specific properties. Rust does this with traits, Java with abstract classes. Traits are doing a great job for generics, but they do not help you structure your objects, they only build the interfaces between them. Often libraries do not use traits extensively, which makes the library hard to expand on, because Rust's type system is strong and hardly allows for anything abstract unless the generic interface is explicitly defined. You can't just derive from a public class, and you can't just make an existing class abstract by changing some keywords around, to use your own type with all of the libraries code. Object orientation in Java is pretty neat, I will have to give it that. It would be nice to have such a system in Rust, of course as minimally as possible.

2 Likes

Personally, I don't miss inheritance at all. You almost never need inheritance and it comes with a lot of potential issues, for some added convenience of not having to write the same methods several times and forwarding it to the wrapped type.

That being said, if you cannot inherit, you need to make heavy use of traits (which are interfaces+) and IMO, Rust has far too few traits, making it sometimes unnecessarily painful to wrap existing types, because you have to create your own trait for certain standard library types. Rust would be easier to use, if it had more traits.

For example, types like BTreeMap and HashMap should have a common interface. They're both concrete implementations of a map, yet the standard library has no such trait. Yes, I could create my own Map trait and have both types implement that trait, but why do I have to do that for something as common as a map?

22 Likes

The "impedance mismatch" of Rust's model vs typical OOP is a stumbling block indeed, but I don't think Rust will change in this area. Data inheritance has been considered in the past, but the designs didn't go anywhere. OO programs also tend to cross-reference objects a lot, which is a pain for the borrow checker, so OO-Rust may not be easier to use. C and Go don't have true inheritance either, and are doing fine.

11 Likes

On the topic of inheritance, I find myself missing a delegation mechanism more than "full blown class hierarchies" (that I don't use anymore in C++). Basically, a way of telling the compiler "implement this trait from this field/method" is all I would personally need.

4 Likes

Well, I don't think C would become adopted nowadays, and Go is not a language that I care much for anyways. Its only selling points are basically go routines and its web ecosystem, which give a lot of web app ideas enough to get started. Rust has the major advantage of the borrow checker, which is something I don't want to miss anymore while programming, by the way. Everyone is sick of null pointer dereferences, and everything needs correct memory access, so I personally think Rust has the right attitude. I also like traits for their minimalism, they do the job exactly, but they need to be promoted or even mandatory. An object should only be able to implement public functions through interfaces, so that library users can use the library with their own extensions.

Also data inheritance seems to be an important feature, I liked how Go did this, there you can just write a struct name into a struct definition to inherit it's fields. Converting to that type is simply done with the dot operator:

type Inner struct {
    count int
}
type Outer struct {
    name string
    Inner
}
...
a := NewOuter()
foo(a.count)
bar(a.Inner)

@kornel do you know about any data inheritance designs which have been developed for Rust? I think this is an important feature for Rust's broader success. It probably played a big role for C++' success aswell.

I feel like that's something you could automate using a clever derive and adding a #[defer(SomeTrait)] attribute above the field.

You just need a way to make sure the procedural macro knows a trait definition before trying to generate the defer code...

1 Like

I actually have an experimental library doing parts of this (I didn't do the delegate proc-macro back then, so you still had to implement a simple trait to delegate a trait :sweat_smile:). I believe others also experimented in similar directions, but to really be ergonomic we'd need a way to get the items of a trait from within a proc macro, ideally without having to annotate or reproduce the trait definition. I guess this requires language support.

Trait delegation feels like a strong use case to me, I hope rust will get to it eventually.

3 Likes

Object oriented programming does make it easy for a simple logic but does not fit in the real world. When inheriting, keeping track of the stuffs happening in the parent can make it hard to fit it thought, and even worse some stuff declared might go unused.

Good story discussing OOP and Rust in story form.

By the way, there can be inner in rust as well. See serde-json, even though writer is considered inner, it could be named anything and hidden from user.

1 Like

I :heart:d @mankinskin's post because I think he has focused on a difficult area in transitioning from OOP to Rust. Many of us have long been aware of this difficulty, but so far I have not seen tooling to assist, or better yet semi-automate, the transition from OOP structures to trait-based ones. Now that procedural macros have landed, it might be possible to provide some such partially-automated assistance or guidance to Rust newbies who are coming from an OOP background.

3 Likes

How about just a tutorial for C++ programmers coming to Rust?

It's not clear to me that adding support to Rust as a crutch for them is a good idea.

When in Rome, do as the Romans. As they used to say.

8 Likes

Sorry if I miscommunicated. I was not suggesting that Rust be modified to add support for OOP; instead I was attempting to suggest that it might be possible to develop tooling to assist programmers used to OOP to instead use Traits. There are a number of threads in this forum where I've posted a response suggesting that the thread's OP read the second Rust Koan (already referenced a few posts up-thread) to understand why OOP is an overly-constraining design approach.

What struck me as most relevant in @mankinskin's longer post up-thread was his identification of the difficulty of OOP programmers in converting to use of Rust's trait system, and the lack of easily-located support documentation to assist in that conversion / re-education. I myself am not able to produce such a tutorial, but perhaps other readers of this thread could do so.

2 Likes

I don't know, perhaps I was lucky to be weaned on languages like Algol, PL/M, Coral and C. I never did buy in to all that object orientation, inheritance and so on to a large extent.

I'd like to think anyone capable of writing C++ competently has the wits to master Rust easily enough. Even if they do whine about missing some little luxuries from home along the way.

2 Likes

Indeed a nice story about OOP. Very well written especially. It is a nice display of composition over inheritance, I believe.

Maybe there is nothing wrong with Rust's object model after all, it's only that most people do not use it correctly, maybe myself included. A few guidelines for how to model problem domains using traits would probably be widely appreciated.

However I am not sure if that will solve the problem for good. Of course it is not the responsibility of the language to prevent bad designs altogether, but they should definitely promote good designs and help avoid bad ones. I don't think Rust is doing that with the trait system right now. It should be enforced that associated functions and methods are implemented through traits, as this is Rust's vehicle for abstraction, which becomes especially important when interfacing with a lot of external crates.

It is difficult to find a design that is backwards compatible, but maybe it is possible to implicitly define a trait for every type implementation. All the public function signatures could be used to construct a trait with the same name as the type. It's currently not possible to have a trait and a type with the same name, however traits and types can only occur in distinct places, so maybe this change could work.

I think this would greatly encourage the use of traits in the Rust ecosystem, and would make it far easier to write generic code with external libraries. I would be interested in your thoughts about this, maybe I am missing something, otherwise I might open my first RFC for this.

4 Likes

Are you suggesting that writing this would not be allowed:

struct Foo {
}

impl Foo {
    fn something () {
    }
}

And that one would have to write:

struct Foo {
}

trait FooTrait {
    fn something(&mut self);
}

impl FooTrait for Foo {
    fn something (&mut self) {
    }
}

I guess we could also get rid of those free standing functions that clutter the place up whilst we are at it.

Add a "class" keyword to clean up the syntax a bit and make in more familiar to many.

Rename the language CRust#.

That should attract a lot of users from other popular languages

:slight_smile:

Well, almost, my suggestion was that this would be valid:

struct Foo;

impl Foo {
    pub fn something () {
    }
}

struct Bar;

impl Foo for Bar {
    fn something() {
    }
}

With the reasoning that public functions are part of the interface of a type, and should therefore be associated with a trait. Trait functions are already public by default, because that is exactly what they are for. They describe an interface to a type.

This would make adding a public function a breaking change, which is very bad for stability.

7 Likes

Well, that is true, but that is already the case for traits. And if that stays a disadvantage for traits then people will be even less inclined to use them.

Also, it makes sense that if the public interface changes, that that could potentially be breaking for depending crates, so I see this as a reasonable error.

@ZiCog I mean, my point is that we need to encourage people to use traits. Right now traits are only used when necessary, which makes it very difficult for third parties to use a given crate for anything other than for the predefined use case, which is very limiting. If every public interface was implemented through a trait, everyone could implement their own version of a type and still use the library.