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

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.

It is doing it, after reading quite some blog posts and rust internals as well as design discussions. People do mention how the choice of traits affects how it is used. For example, in the latest tokio's blog post (tokio 0.2) pointing to a discussion of trait design for async/await:

Trait can do a lot of stuff when talking about composition over inheritance. But what I felt is that there are a lot of complexity to get started, to know what functions exists, one probably need to go through the extensive list of traits, which is maybe a rather huge space O(n*m) compared to searching in inheritance (only need to go through parents + their troublesome states). Maybe some trait searcher or discovery can help?

For example, I did not know to_string() comes mainly from Display by just reading at to_string() function docs only, for quite some time. Even though the answer is just a page scroll above but it took me some time to find that (later I know that it is also in the book). https://doc.rust-lang.org/std/string/trait.ToString.html#tymethod.to_string

Regarding the breaking changes, I believe that is a semver issue rather than a trait issue.

1 Like

But traits are designed to be an immutable public interface, meant for communicating intent. The public interface for anything else should be allowed to grow without a breaking change to allow for new functionality, otherwise one of three things would happen: crates will regularly make major version increments, ctates will stagnate, crates will add a whole bunch of traits for the sole purpose of adding a few methods. None of these are appealing.

Adding new methods currently does not suffer any of these problems because in general, adding a method can't break downstream crates.

One more thing, how would this play with generics? Currently traits are tightly coupled with generics, which is why they are so powerful, but I can't see a way to do that with your proposal.

I was referring to @mankinskin's proposal, not traits as they are now

6 Likes

This is not how I was introduced to traits. To me traits are a common interface for multiple types. Keeping them stable is already the library's responsibility.

If a library designer is adding a new public function, they can put it in a new public trait. This is even to be encouraged, because it is according to the single responsibility principle. One trait should only describe one kind of behavior. If a new behavior is added, a new trait should be added.

When a public function signature changes, then that is already breaking anyways, so this wouldn't change anything about this.

Why should I have to add a new trait to my crate interface just to get the method syntactic sugar? I could just make a free function. Yes, it would be less ergonomic, but it would also be significantly easier to maintain and to explain. I wouldn't have to document what a conforming implementation looks like, as would be the caee for designing a new trait.


How would your proposal interact with generics? If it doesn't, then it is a useless burden on library writers.

2 Likes