Syntactic sugar for delegation of implementation

Sorry if something similar has already been proposed.

In short

A nice feature of usual OO languages is the implicit code reuse through inheritance: the fact a subclass does not need to redeclare any method existing in its base class provided the behaviour is the one expected. Wouldn’t it be a nice feature if Rust had some syntaxic sugar to mimic this ?

Composition and inheritance

When I have:

// java-like language
interface Foo {
    void doSomething();
}

class Bar : Foo {
    void doSomething() { ... }
}

The nice thing is that in class Baz : Bar {} method doSomething() is implicitely available as a method of Baz. A well known limitation of this pattern and one of the main arguments for the composition over inheritance principle is that it cannot be extended to import methods from different classes (unless of course your language offers multiple inheritance). That’s why the compositional equivalent is often presented as a better practice:

class Baz : Foo {
    Bar bar;
    void doSomething() { this.bar.doSomething(); }
}

Rust has no type inheritance (not yet?) but composition can of course be used in a similar manner:

trait Foo {
    fn do_something(&self);
}

impl Foo for Bar {
    fn do_something(&self) { ... }
}

struct Baz {
    ...
    bar : Bar
}

impl Foo for Baz {
    fn do_something(&self) {
        bar.do_something();
    }
}

Now the main drawback for this pattern in Rust and in most OO languages is the boilerplate implied. Basically you need to write obvious code that just delegates each method call to a given field again and again. This is not a big deal if your interface/trait is short. This starts to be annoying when it gets longer.

Automatic delegation for implementations

What if we had some nice syntactic sugar to do this automatically?

impl Foo for Baz use self.bar {}

One nice feature is that when Self is used for other parameters it can also benefit from the sugar:

trait Foo {
    fn do_something(&self);
    fn do_something_else(&self, other: &Self);
}

impl Foo for Baz use self.bar {
    // also implicitely defines
    // fn do_something_else(&self, other: &Self) {
    //     bar.do_something_else(other.bar);
    // }
}

We can even imagine using a general expression instead of a single field provided the type of the expression implements the trait:

fn get_bar(x: i8, y: u16, foo: &Foo) -> Bar { ... }

impl Foo for Baz use get_bar(self.x, self.y, self) {
    // fn do_something_else(&self, other: &Self) {
    //     get_bar(x, y, self).do_something_else(get_bar(other.x, other.y, other));
    // }
}

One of the potential issues is that it may not work very well with the move semantics. However it’s easy to combine implementation delegation and actual implementation so one can write the methods that cannot or must not be generated automatically. Everything missing is left for delegation.

Possible extensions

  1. This could benefit from a potential “combined implementation”:
trait Qux {
    fn do_something_complex(&self);
    fn do_something_fast(&self);
}
...
impl Foo + Qux for Baz use self.bar
{
    fn do_something_fast(&self) {
        // implement with this one explicitely
    }
    // the 3 remaining methods are delegated to self.bar
}
  1. If we can imagine delegation for method parameters, we can of course think about something similar for the method result.
trait Foo {
    fn new(name: &str) -> Self;
    fn do_something(&self) { ... }
    fn do_something_else(&self, other: &Self);
}

impl Foo for Baz use self.bar, Baz { bar: return, x: 4, y : 0 } {
    // implicitely defines
    // fn new(name: &str) -> Self {
    //   Baz { bar: new(name), x: 4, y : 0 }
    // }
}

Advantages / Disadvantages

  • As in usual OO any addition of new methods on a trait only implies new method implementations at the “super type” level. All delegating implementations are unchanged.

  • This is equivalent to code reuse through multiple inheritance with limited amount of code. Now this implies no inheritance relation at alI: it is only about code reuse NOT about type substitution/inheritance.

  • This might be considered as too implicit when Rust tries generally to be explicit.

  • This might not be so useful if the vast majority of traits contain only one method.

5 Likes

It seems like you’re just looking for trait defaults or did I misunderstand? Like this:

use std::fmt::Display;

trait Print where Self: Display {
    fn print(&self) { println!("{}", self) }
}

impl Print for i32 {}
impl Print for f64 {}


fn main() {
    let i = 9;
    let f = 3.4;
    i.print();
    f.print();
}

playpen

Also, inheritance for rust was being analyzed in this blog post.

4 Likes

trait Print where Self: Display {

pretty sure you can just do

trait Print: Display {

Yeah, you’re correct.

This is not what I meant. My post must be very confused :disappointed: I’ll try to edit it. I will also read the blog post in details but I doubt this is really related. My proposition is not about inheritance if you mean type hierarchy and type substitutionality. It does not create any kind of relation between types. It is only about some syntactic sugar for some form of code reuse that can possibly be achieved with inheritance in a java-like language.

I wasn’t thinking about inheritance particularly though you were speaking about it. It appears you want to define a function which automatically does something for some new type without having to manually implement it on the new type. Trait defaults do exactly that. You only have to implement it once and only override it if you manually need to. What other problems are you trying to solve?

If you could clarify below, that would be helpful. The OP is kinda unwieldy.

Let’s try with a more concrete example.

trait Flying {
    fn fly(&self);
}

impl Flying for Insect {
    fn fly(&self) { ... }
}

trait Illuminating {
    fn illuminate(&self);
}

impl Illuminating for Luciferin {
    fn illuminate(&self) { ... }
}

My concern is how to reuse implementations of illuminate from Luciferin and of fly from Insect for struct Firefly using a limited amount of boilerplate ?

My idea was that we could simplify the composition pattern:

struct Firefly {
    ...
    luciferin: Luciferin,
    insect: Insect
}

impl Illuminating for Firefly use self.luciferin {}
impl Flying for Firefly use self.insect {}

I may be missing something but I cannot figure out how trait defaults can address the same issue. Of course something like this may work:

trait Flying : FlyingPrerequisite1 + FlyingPrerequisite2 + ... {
    fn fly(&self) { ... }
}

impl Flying for Firefly {}

but it assumes Firefly already implements some traits except if the default implementation is trivial. My proposal does not assume any previous traits. It just need a sort of converter (here the use self.insect for example) to transform any instance of Firefly into an instance of a type that already implements Flying (here Insect). so the delegated implementation is in another type and there is no specific implementation stuck in the trait.

Is this new message clearer? Do I propose something already existing in another form?

1 Like

This is calling for compiler plugin directive, like #[DeriveFrom(baz)] to derive methods from given field in a struct. No need for RFC, just write a compiler plugin and publish it to crates.io.

Why not just create a default which suits your generic purpose? Isn’t that what setting up inheritance is basically doing? Like here.

Granted, a more complex hierarchy might be more convenient that way. I don’t know.

The reason is that this only works if the implementation is independent of the implementing struct. Assume that fly() accesses/modifies some fields of Insect. Default implementations don’t work in this case, because they can’t access any fields.

1 Like

Florob is correct. And again, even if you can cope with a trivial implementation that cannot access any internal state, your trait default can only benefit a type that needs that specific implementation. It's natural that the implementation of fly for Firefly can reuse the one for Insect. It's very improbable that your Dragon needs the same implementation. With my proposal there is no such a problem. Maybe you want to provide your own specific implementation or maybe you can delegate it to a FlyingReptile type. With your solution you have no choice but defining a single default implementation that might benefit most of the future implementing types.

Thank you for the suggestion. Unfortunately I'm afraid I do not have the expertise for creating a compiler plugin. Now you could tell me this is the good opportunity to learn. But at least I'd like to know if it could benefit the community. Is there anybody that may find this proposal interesting / useful?

I would find this very useful. D has a similar feature fwiw. It is one of these things that are not necessary but super nice to have and makes some patterns pleasant to write.

I really like this idea, my only worry is that I could accidentally implement a method when I didn’t plan to.

For example, I start with this initial definition:

trait Foo {
  fn do_a();
  fn do_b();
}

struct X {}

impl Foo for X {
  fn do_a() {
    ...
  }

  fn do_b() {
    ...
  }
}

struct Y {
  x: X,
}

impl Foo for Y use self.x;

If I then redefine Foo and X like so:

trait Foo {
  fn do_a();
  fn do_b();
  fn do_c();
}
...
impl Foo for X {
  fn do_a() {...}
  fn do_b() {...}
  fn do_c() {...}
}

Then Y will implicitly get the do_c behavior. I would propose the syntactic sugar be a little more explicit, line so:

impl Foo for Y use self.x for do_a, do_b;

This way, when do_c is defined on Foo and X, the compiler will fail on Y's implementation of Foo.

It still saves on writing lots of boiler plate, but you get the advantage of being explicit.

Disagree. Inheritance is a misfeature.

1 Like

Argh. I tried to do this. I'm going to just post my whole question as it has a pretty good compelling example:


Let's say that I'm prototyping a new struct, and I can emulate it with a Vec to get off the ground. I'd also like to expose an iterator. That's great, as I can just reuse the iter method from the slice:

struct SparseArray(Vec<u8>);

impl SparseArray {
    fn iter(&self) -> ??? {
       self.0.iter()
    }
}

A problem arises when I go to define the result type; I have to expose that I'm using a slice iterator! I plan on implementing this using something more suitable, so I don't want consumers to actually know that there's a slice under the hood. Maybe my internal representation will change a few times, so I want to hide the implementation details, like a good programmer.

The best solution I am aware of is to create a newtype around slice::Iter, but that presents a different challenge: now we have to define the Iterator trait for the newtype:

struct SparseArrayIter<'a>(slice::Iter<'a, u8>);

impl<'a> Iterator for SparseArrayIter<'a> {
    type Item = &'a u8;

    fn next(&mut self) -> Option<&'a u8> { self.0.next() }
}

Yay! I've wrapped my iterator.

But I should probably wrap size_hint... oh, and it turns out that nth has a specialized implementation so I should call it too. And what about the other ~50 iterator methods?

A proposal

I'd like to be able to write something like this:

#[delegate(Iterator, to = "self.0")]
struct SparseArrayIter<'a>(slice::Iter<'a, u8>);

And the ever-helpful compiler can just write out all of those annoying shim methods. I started a syntax extension to do this, but immediately ran into a problem: there's no way to get information about other types.

So, what alternatives do we have? I think that an RFC is needed, in order to have a hook with the appropriate data available.

Note that I've used a trait here, but I think that conceptually it should work with delegating 1 or more methods to any field of a struct.

Inheritance for the sake of code reuse is a misuse of inheritance, certainly. However, inheritance does have a place, if you follow Liskov. However, you can't really argue that by misusing inheritance, you do get code reuse very easily. This topic is all about having an equally-easy way to get reuse without having inheritance.

As does Haskell, as "generalized newtype deriving, if I understand correctly.

1 Like

I understand the concern but I think implicit implementations of new methods can also be considered as a desirable feature. A solution could be to allow both:

// Please implement everything. I'm an adult. I understand the risks.
impl Foo for Y use self.x for ..;

If a plugin is undoable now, is it really worth it waiting for a hypothetical improvement of syntax extension rather that just adding this feature as a syntactic sugar?

I think we agree. Maybe my use of the word inheritance introduced misunderstanding with some participants. What I call inheritance (possibly incorrectly, tell me) encompasses both subtyping and a certain form of code reuse. Now I think we are several to agree that precisely it is a flaw of many OO languages that they entangle subtyping and code reuse whereas they should exist as two orthogonal concepts.

Is there actually a time that you need to use the parent as a T: Flying + Illuminating? If not you could just pass a references to the individual member when you want to call a function that needs one of those traits.

Alternatively, do Flying and Illuminating actually need to be completely separate traits like that? If you find yourself needing both a lot then maybe it’s not worth separating them.

@DanielKeep has made some interesting and useful experiments in this direction:

https://danielkeep.github.io/rust-custom-derive/doc/newtype_derive/index.html

It’s a lot of macro magic for it to work in a limited way, an RFC for language support seems warranted, it would be a big ergonomic win for composition over inheritance.

1 Like

In simple cases you can use dereference and macro (i don't think this is a good style, but works):

macro_rules! emerge {
    ($from:ty => $to:ty, $by:ident) => {
    	impl Deref for $from {
    	    type Target = $to;

    	    fn deref<'a>(&'a self) -> &'a $to {
    	        &self.$by
    	    }
    	}
    };
}

struct Firefly {
    luciferin: Luciferin,
    insect: Insect
}
emerge!(Firefly => Luciferin, luciferin);


fn main() {
	let ff = Firefly { luciferin: Luciferin, insect: Insect};
	ff.illuminate();
}

The main problem is you can implement Deref this way once only.

What about to add sugar like dereferencing but which can be implemented many times (like From) and uses to level up methods (struct field references returns)?

Unless I’m hugely misreading it… You want something like multiple automatic derefs to be possible at once? But more fine-grained as to which traits apply to which member in the type you’re extending.

Dlang has something similar; although it’s tied to the type definition time. The implementation might be of interest:

  • opDispatch(string op: "method_name")() (for forwarding specific method calls; although with some meta-programming you could automate all the decisions for which call to forward where);
  • and alias <member> this ((an incomplete) way to provide subtyping in value types via composition).
1 Like