Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Another take on delegation. #3133

Closed
N4tus opened this issue May 28, 2021 · 2 comments
Closed

Another take on delegation. #3133

N4tus opened this issue May 28, 2021 · 2 comments

Comments

@N4tus
Copy link

N4tus commented May 28, 2021

Another take on delegation

This is my take in specifying a delegation strategy for rust.

I am unsatisfied with the current proposals and want to share my take on this problem.
Manual delegation requires a lot of code. My example below only implements one of the different implementations of IntoIterator for Vec<T>. It also is not clear by the first glance, that it is just a delegation and does not really introduce new functionality.

My idea

This:

struct MyVec<T> {
    inner: Vec<T>
}

impl<'a, T> IntoIterator for &'a MyVec<T> {
    type Item = <&'a Vec<T> as IntoIterator>::Item;
    type IntoIter = <&'a Vec<T> as IntoIterator>::IntoIter;

    fn into_iter(self) -> Self::IntoIter {
        <&'a Vec<T> as IntoIterator>::into_iter(&self.inner)
    }
}

turns into this:

struct MyVec<T> {
    inner: Vec<T>
}

impl<'a, T> IntoIterator for &'a MyVec<T> { 
    delegate IntoIterator::* to &'a self.inner;
}

This is a simple example of a delegate implementation. It consists of a delagate keyword followed by it items to delegate (methods and types), a to keyword followed by a "receiver".
In order to have more fine-grained control over what is delegated, the syntax mimics that of a use statement. Therefore you can for example also select specific methods by name instead of everything.

impl<'a, T> IntoIterator for &'a MyVec<T> { 
    // if allowed, you can take all types
    delegate IntoIterator::{fn::into_iter, type::*} to &'a self.inner;
}
impl<'a, T> IntoIterator for &'a MyVec<T> { 
    // or you list them by name
    delegate IntoIterator::{fn::into_iter, type::{Item, IntoIter}} 
          to &'a self.inner; // (yes, vertical alignment)
}

As far as I know, you can't have a method named type or fn. So this would not overshadow a potential method delegation. Grouping methods and type in their own "name space" allows to only take all methods or types without naming them all (IntoIterator::fn::*).

The receiver currently takes a self parameter. For a classic struct this is not really necessary but for a tuple struct just having a number there would be weird.

impl<T> MyVec<T> {
    fn get_inner(&self) -> &Vec<T> {
        self.inner
    }
}

impl<'a, T> IntoIterator for &'a MyVec<T> { 
    delegate IntoIterator::* to fn get_inner;
}

If the receiver is a method it has to be declared with the fn keyword. The explicit reference is missing since the exact type is declared in the return-type of the function. Also the self parameter must match the type of Self.
A confusion thing with a field-delegation is what the reference on it means. It allows for refinement of the type of the field, to specify exactly to where to delegate. In my example it is &Vec<T> but it could also be a &mut Vec<T> or just a Vec<T>. However it still can delegate to methods which take a different self parameter if possible. For example an owned inner field can be used by value, mutable reference, and immutable reference.
Method-receiver do not make this possible, you would have to have multiple methods for that.

trait Example {
    fn by_ref(&self);
    fn by_mut(&mut self);
    fn by_owner(self);
}

struct Inner;
impl Example for Inner {
    fn by_ref(&self) { todo!() }

    fn by_mut(&mut self) { todo!() }

    fn by_owner(self) { todo!() }
}

struct Outer1(Inner);

impl Example for Outer1 {
    delegate Example::* to self.0;
}

struct Outer2(Inner);

impl Outer2 {
    fn get_ref(&self) -> &Inner {
        &self.0
    }
    fn get_mut(&mut self) -> &mut Inner {
        &mut self.0
    }
    fn get(self) -> Inner {
        self.0
    }
}

impl Example for Outer1 {
    delegate Example::* to fn get, get_mut, get_mut;
}

You can use either one field-receiver or multiple method receiver in on delegation statement. The return type of the first on listed, dictates the delegation target. Also only its type-definition are used and no others. Delegation RFC #2393 used something that looks like a closure, to delegate to something other than a field. But I don't want to introduce a new location where code can be executed. Functions and Closures are enough. I think having something like an auto-referencing of methods-receivers is too confusing. If it turns out useful it can be added later.
Method-receivers need compatible bounds with the current implementation. I believe the compiler can figure that out, but I not 100% about this.

Since it is allowed to only delegate to a specified subset of methods/types a trait implementation could be incomplete. I should be able to still add normal implemented methods/types in an impl with a delegation statement. But delegating to method that is not part of the trait currently implemented should raise a compiler error. In general I think it should be a compiler error if multiple types/methods are defined in an implementation via a delegation. Therefor you are not forced to explicitly list all other methods if you only want to override a small subset. That means manually implemented methods always override delegated ones. But I don't want to allow delegations overriding each another. It would result that the order of definition matters and a reader needs to know what items are in a trait to understand which ends up ending in the implementation.

One thing I want to allow is to delegate an implementation to a separate trait that just happened to have (mostly) the same requirements to be implemented. For example suppose there are two graph libraries that specify a graph trait and operations on them. The operations are different and in my application I want to use both of them, so I implement the trait for on of the libraries and can use that implementation as delegation target for the second one. Or most of it, since I can add the rest manually.

You can also use Self or any other struct instead of a trait to delegate from. Then it should only delegate to accessible methods directly defined in the impl for said struct.

You can also delegate not only in an trait implementation but also in a inherent implementations. Doing so does not include any types if delegated from a trait as the entire type name space is not accessible. This allows pulling some of the methods defined in a trait into the inherent implementation to be used without useing the trait. You wouldn't need the fn name space, but I think for consistency it should still be used. Delegating from a struct in a trait then restricts the usage to only public methods.

Cycles with delegation are possible, but if they occur the manual defined method takes priority.

Associated methods do not take part in the delegation process.

Notes on complexity

There is definite some new complexity involved in writing a delegation. But I think reading it is almost self-explanatory.

Keywords

It requires only two new keywords: delegate and to (could be a weak keyword). The delegate keyword makes sense as it is, but there might be a better alternative to to.

Other syntax

Another advantage is, that this strategy does not modify already existing syntax like RFC #3108 does.

@Diggsey
Copy link
Contributor

Diggsey commented May 28, 2021

Most traits in Rust only declare one or two items. Does this actually save that much typing to be worth the extra complexity?

IME, a lot of cases where delegation would be useful would be when you have several traits and you want to delegate them in the same way.

@burdges
Copy link

burdges commented May 28, 2021

I think both this and #3108 can be closed. Lang team recently closed #2393 not for the syntax, but for bandwidth concerns, so immediately reopening issues with less informative discussion seems pointless.

A useful step would be a forum or blog post that honestly summarizes the technical discussion in #2393, including that procmacro crates already deliver a substantial portion of this functionality. It's maybe worth noting syntax conflicts like everything use based, but ignore the bikeshed for now.

@jyn514 jyn514 closed this as completed Jun 3, 2021
@jyn514 jyn514 mentioned this issue Jun 3, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants