3 weeks to delegation! Please help

Delegation of implementation (RFC 1406) was opened on 13 Dec 2015 and has been talked about in various forms for a while before that. It is on the 2017 Roadmap in the Traits section.

The impl period starts Sep 18, so allowing time for the lang. team to review + FCP, we only have 3 weeks to get this RFC ready, if we want it implemented this year or early next year.

I’ve forked the RFC and I’m making progress refactoring it, but I very much doubt I’ll get it finished within the next 3 weeks. So, I need your help! I’ve posted about Collaborative RFCs and I’m not the first person to think of it, so let’s band together and apply it to getting this long awaited and desired feature into Rust.

@AndrzejB just asked why not add this? And many others have asked the same. Often people are pointed to this RFC, but without guidance on how they can help get it accepted. Whatever your experience with Rust, you’re welcome to help get this feature approved.

There’s no question too stupid, ask away! Don’t know how to make git/github do what’s needed? I’m happy to help. Also, even if you don’t have time to get involved, feel free to submit a “drive by” PR or leave a comment.

Please go here and comment/ask for push access to the repository.

7 Likes

I’ve been thinking about delegation a lot actually and have been meaning to get in touch with you on this. I agree with the comments on the original RFC’s thread that that RFC has too much going on and underspecifies a lot of what it proposes, and I think those are the main blockers here. The more I reread the original RFC and the new version of it, the more I feel like what the RFC needs is not a checklist of tweaks/fixes/adjustments/additions, but a fresh rewrite from scratch with a more limited scope (that’s easy to extend later), fewer digressions and more clear/precise wording. Do you disagree with that? Would it be useful if I took the time to write out “my version” of the RFC properly?

7 Likes

I’m totally on board with that, please do!

This is definitely overcomplicated. I’m familiar with many, many programming languages and when I read code samples from RFC, I have no idea what they mean.

The idea with 'use [for method1,method2,method3]` is nice, intuitive and saves a bit of work (mainly types), but it could be done by IDE as well.Other usecases are definitely too complex.

4 Likes

@elahn Thanks so much for taking this on!

1 Like

Okay, here’s what I’ve got after only several minutes of drafting and proofreading and zero peer review:

Summary

Introduce sugar for “delegation”, making it ergonomic to “favor composition over inheritance” by explicitly delegating most or all of a trait’s impl block to a member field that already implements that trait.

Motivation

I think elahn’s longer version of contactomorph’s original Motivation section is already very good, so: https://github.com/elahn/rfcs/blob/delegation/text/0000-delegation-of-implementation.md#motivation

Guide-Level Explanation

Whenever you have a struct S, a trait TR, and S has a field f of type F, and F already implements TR, you can implement TR for S using the contextual keyword delegate:

impl TR for S {
	delegate * to self.f;
}

This is pure sugar, and does exactly the same thing as if you “manually delegated” all the items of TR like this:

impl TR for S {
	type Item = <F as TR>::Item;
	...
	const MAX = <F as TR>::MAX;
	...
	fn foo(&self) -> u32 {
		self.f.foo()
	}
	fn bar(&self, x: u32, y: u32, z: u32) -> u32 {
		self.f.bar(x, y, z)
	}
	...
}

To delegate most of a trait, rather than all of it, simply delegate * and then write the manual implementations for the items you don’t want to delegate.

impl TR for S {
	delegate * to self.f;

	fn foo(&self) -> u32 {
		42
	}
}

Aside from the implementation of foo(), this has exactly the same meaning as the first example.

If you only want to delegate specific items, rather than “all” or “most” items, then replace * with a comma-separated list of only the items you want to delegate. Since it’s possible for types and functions to have the same name, the items must be prefixed with fn, const and type as appropriate.

impl TR for S {
	delegate fn foo, fn bar, const MAX, type Item to self.f;
}

This also has the exact same meaning as the first example.

Reference-Level Explanation

A delegation statement can only appear inside a trait impl block. Delegation inside inherent impls is left as a future extension.

Delegation must be to a field on self. Other kinds of implementer expressions are left as future extensions. This also means delegation can only be done on structs for now.

Delegation statements must be the first items in an impl block. There may be more than one delegation statement, but they must all be at the top.

A delegation statement always consists of:

  • the contextual keyword delegate
  • either a *, or a comma-separated list of items being delegated
  • the contextual keyword to
  • an implementer expression of the form self.<field name>
  • a semicolon

An “item being delegated” is always two tokens. The first token must be either fn, type or const. The second is any valid identifier for a trait item.

The semantics of a delegation statement should be the same as if the programmer had written each delegated item implementation manually. For instance, if the trait TR has a default implementation for method foo(), and the type F does not provide its own implementation, then delegating TR to F means using TR’s implementation of foo(). If F does provide its own implementation, then delegating TR to F means using F’s implementation of foo(). The only additional power granted by this feature is that delegate * can automatically change what items get implemented if the underlying trait TR and type F get changed accordingly.

Possible Future Extensions

There are a lot of possibilities here.

  • Delegating inherent impls.
  • Delegating to getter methods instead of fields.
  • Delegating to static values or functions.
  • Delegating to arbitrary expressions.
  • Delegation an inherent impl to a trait impl, or vice versa.
  • Delegating a method foo() to a differently-named method bar() that happens to have the same signature.
  • Delegating “multiple Self arguments” for traits like PartialOrd, so that delegate * to self.f; would desugar to something like self.f.partial_ord(other.f)
  • Delegating trait fields, once that feature is implemented.

We probably don’t want to do most of these, as this is supposed to be a pure sugar feature targeting the most common cases where writing out impls is overly tedious, not every conceivable use case where “delegation” might apply. However, the author believes it likely that a few of these extensions will happen, and the proposed syntax is intended to make it as easy as possible to add any of these.

Alternatives

The biggest non-syntax alternative is only supporting delegation of methods, and not associated types or consts. This author prefers to support all “trait items” because the whole point is to “make trivial wrapper impls trivial”, even if you’re implementing a trait like Iterator which has an associated type as well as several methods.

There are also a lot of alternative syntaxes. Here are some of the options this author consciously chose not to use in this RFC:

  • impl TR for S use self.F { ... } was criticized in the first RFC’s comment thread for looking too much like inheritance.
  • impl TR for S { use self.F; ... } was criticized in the first RFC’s comment thread for ruling out the possibility of use statements inside impl blocks.
  • impl TR for S => self.F; and impl TR for S => self.F { ... } This is good for delegating an entire trait impl, but when used for partial delegation where the remaining implementations are inside the curly braces, this starts looking like inheritance again, appears to put implementation details in the signature where they don’t belong, and I believe would be relatively easy to overlook compared to most of the other syntaxes.
  • fn method = self.field.method; This syntax was suggested for delegating a single item. It’s not clear how to extend it to delegating multiple items, “most” items or all items in an impl.
  • Various attribute syntaxes like #[delegate(foo=S)]. Most of these made it hard to tell what was the item being delegated and what was the field being delegated to. This also seems like it would lead to “stringly typed” attribute syntax like #[delegate(foo="self.f.foo()")] if we tried to make it cover most of the possible future extensions. Also, attributes for an impl block would normally go outside the impl block, but since delegation is purely an implementation detail it again seems strange to put it outside the block. Finally, an attribute would be appropriate if this feature could be implemented as a proc macro someday, but delegation cannot because it requires “looking outside” the impl block to see all the items in the trait being implemented.

Many of these syntaxes were never “rejected” in the original RFC’s comment thread and are likely still on the table. This list merely describes the author’s rationale for preferring delegate ... to ...; over all of these alternatives.

Drawbacks

  • Yet another (contextual) keyword proposal.

  • This is basically a new way of writing trait implementations, which is something we can already do. Having two ways of doing the same thing is often undesirable.

  • If too many of the future extensions are implemented, this could easily become an overly complex feature.

Unresolved Questions

  • Is it useful and/or feasible to allow delegation statements to appear anywhere in the impl block, rather than all at the top?

  • Is “contextual keyword” the right term and mechanism for delegate and to as proposed here?

  • Do we want to support all kinds of trait items, or should we be even more minimalist and support only methods in the first iteration?

7 Likes

I haven’t followed the original RFC. Does this comment basically sum up what’s stalled and why?

I’d like to see delegation appear in some form too. Then I can retire my little compiler plugin for doing a much smaller and less general version of this.

I’m not sure how much time I’d have to actually get on board, or how much time you’ve got to consider alternatives, but I think proposing an attribute macro rather than a new first-class language feature might have more success getting through given the timeframe.

2 Likes

It's a good summary of what needs to be addressed. Why the RFC stalled isn't important right now, but some of the reasons can be inferred from this.

I completely agree with @Ixrec about attribute syntax:

Anything you have time to contribute, even comments here would be appreciated. Nothing is off the table, but I like @Ixrec's approach of a simple base feature and everything else as possible extensions. Nonetheless, we should make sure the base feature is forward compatible with possible extensions, so suggest away!

1 Like

Those are good points :+1: Without access to the trait definition you do lose a lot of the benefits of delegating impls if it’s almost as much effort to delegate them as it is to implement them. I don’t think you’d necessarily end up with a stringly-typed API though, since the stuff inside the attribute is just a token tree, that you would treat as source that gets compiled. You would need to be careful though.

For instance, something like:

#[delegate(to = self.meta())]
impl SomeTrait for SomeStruct {
    type AssociatedTypeA = SomeType;

    #[delegate]
    fn inherent_method_a(&self) -> Result<(), Error>;

    #[delegate(to = self.meta_mut())]
    fn inherent_method_b(&mut self) -> Result<(), Error>;

    fn inherent_method_c(&self) -> Result<(), Error> {
        /* not delegated */
    }
}

Which expands to:

impl SomeTrait for SomeStruct {
    type AssociatedTypeA = SomeType;

    #[delegate(to = self.meta())]
    fn inherent_method_a(&self) -> Result<(), Error>;

    #[delegate(to = self.meta_mut())]
    fn inherent_method_b(&mut self) -> Result<(), Error>;

    fn inherent_method_c(&self) -> Result<(), Error> {
        /* not delegated */
    }
}

Which expands to:

impl SomeTrait for SomeStruct {
    type AssociatedTypeA = SomeType;

    fn inherent_method_a(&self) -> Result<(), Error> {
        self.meta().inherent_method_a()
    }

    fn inherent_method_b(&mut self) -> Result<(), Error> {
        self.meta_mut().inherent_method_b()
    }

    fn inherent_method_c(&self) -> Result<(), Error> {
        /* not delegated */
    }
}

Could be implemented now, but is much less useful than implementations that have access to the trait definition and/or syntactic sugar.

So my question is: for a minimal useful implementation do we need:

  • Access to the trait definition
  • Syntactic sugar

The answer may well be yes to both of those, in which case my suggestion of just use an attribute macro isn’t really helpful.

2 Likes

I’m sure all this came up already in the 18 months that RFC has been around though :slight_smile:

Maybe we should update some of the examples to use things other than Encodable, since it’s deprecated. It’s not a big thing, but I would be happy to do that.

1 Like

Go for it!

1 Like

A couple of points

Project                 Occurences of "delegating methods"
rust-lang/rust          845
rust-lang/cargo 	38
servo/servo 	        314

I think it would be a good idea to get these numbers in percentages (i.e. out of how many total methods), since it would help put the magnitude of this issue into perspective.

Secondly, wasn’t there effort to implement inheritance in Rust? If so, wouldn’t that make this RFC redundant?

Is the word “delegate” really appropriate in this context? I associate it with a complicated and confusing feature of C# that seems like it does a lot more than this (to be fair, I have never actually learned C#). This proposal is just syntactic sugar for wrapper functions.

I like the idea of having syntactic sugar for wrapper functions, to be clear, I just don’t want to give people the impression that this does more than it really does.

(Obligatory paint color suggestions:

impl TR for S {
    use self.f::TR;
}

impl TR for T {
    use self.f::foo;
    use self.f::bar;
    use self.f::MAX;
    type Item = /* ... */;
}  

I don’t think use already means something else in this context.

I'd like to see percentages too. I suspect no one's gotten around to it simply because motivation was never a major concern with this RFC.

I'm not aware of any effort to add "inheritance" to Rust. But many or all of the useful features that "inheritance" typically encompasses either already exist in Rust or have been proposed/discussed various times. Traits and trait objects obviously provide interface inheritance and dynamic dispatch/runtime polymorphism. Implementation inheritance, even in OOP languages, is typically better off as composition so Rust is unlikely to add that directly. The times when implementation inheritance is most useful are mostly:

  • When you want certain efficiency or memory layout guarantees that OOP languages often give. In Rust, the many proposed solutions to that problem are typically lumped together under the dubious term "virtual structs". See https://github.com/rust-lang/rfcs/issues/349 for the exact constraints and ideas so far. It's likely that one or more features will be added to address this problem, and the "trait fields" idea is probably going to be one of them.
  • When you want to "reuse code" from another type without writing a lot of tedious wrapper code. That's the use case this delegation proposal targets. So as far as I know, this does not overlap with any of the other proposals.

To me, C# delegates are a really weird name for what I normally call "an event handler" or "the observer pattern" in other languages like Javascript and C++ where it's not a core language feature. As far as I know, "delegate" is not used that way by any other language, so I'm not personally worried about confusion. I'm also not aware of any other good names for this feature, though that may just be because there hasn't been much brainstorming for it.

1 Like

I like the proposal to use a new, semantically-clear keyword, though arguably delegate is a little too long. And I think overall this is a much nicer proposed syntax than the alternatives you’ve listed (though I didn’t read the original proposal thread).

I have a few quibbles, and a quickly sketched alternative syntax that I believe resolves them:

  • The items being delegated (* or fn foo, fn bar...) should be listed after the delegated-to member, because, to me, the key information in such a declaration is “this trait is implemented by this member,” not the specific names of the members of the trait.
  • When members are explicitly listed rather than globbed, the line quickly becomes long and difficult to read.
  • The pattern of using a glob but partially overriding it seems potentially confusing.

Here’s my proposal:

We introduce delegate blocks, which are like impl blocks, except that their definitions are “forwarding” rather than from-scratch. A delegate block declaration is the same as an impl block declaration, but uses the delegate keyword in stead of the impl keyword:

delegate TR for S {
    ....
}

(The reason for introducing a new type of block is twofold: (1) I think that forwarding or delegating is simply very semantically different from implementing, and (2) this prevents any confusion or ambiguity, now or in the future, with the syntax of impl blocks.)

Within the block, members of TR are aliased to members of S, or submembers (recursively):

delegate TR for S {
    fn foo = self.foo;
    fn bar = self.baz;
    fn baz = self.f.g.h.baz;    // Equivalent to `impl TR for S { fn baz(args...) { self.f.g.h.baz(args...) } }`
}

When the names of the members of TR being aliased match the names of members of S or some subobject, the following syntax may be used:

delegate TR for S {
    fn bar = self.f.*;                           // Equivalent to `fn bar = self.f.bar;`
    type Item = <S as F>::G::*;                  // Equivalent to `type Item = <S as F>::G::Item;`
    [fn foo, const MAX] = self.*;                // Equivalent to `fn foo = self.foo; const MAX = self.MAX;`
}

To delegate an entire trait to a member of S, additional syntactic sugar (replacing the entire block with a single alias) can be used:

delegate TR for S = S.f;

This would be equivalent to:

delegate TR for S {
    [fn foo, fn bar, ...... (all members listed explicitly)] = S.f.*;
}

It is an error to use this non-block sugared syntax in conjunction with any other delegation on TR.

1 Like

A shorter alternative for the delegate keyword would be fwd (short for forward). Not that I want to go down the Perl route of minimizing the character count of every language construct…

1 Like

I’m glad people are working on this space, but I want to note that I still have the concern I described on the other RFC thread about how, right now, I feel a real lack of clarity around what self means in this syntax. At least this should be described with more clarity and with consideration of future extensions it may enable or foreclose.

1 Like

Now that you mention it, I agree with that. I guess my proposed syntax would now be delegate to self.f for *;

Now that's interesting. AFAIK delegation is just one way of implementing something, this delegation sugar is intended to be interchangeable with manually written delegations, and it's not supposed to be something client code is aware of. What's the semantic difference you see? I didn't see any proposed semantic differences in your post, so I'm not really sure how to argue this other than "it's sugar, it has no semantics beyond what it desugars to".

We may need more people to chime in on whether this is true, but to me the pattern of delegating almost all items except for one or two that you want to change is a very common use case and valuable part of the feature. I also don't want the transition from "delegating everything" to "delegating all but one thing" to require replacing * with an exhaustive list of all the other items.

Did you have a less confusing syntax in mind for this use case, or do you not want to support it at all? IIUC your proposal simply doesn't support it.

2 Likes

Could you expand on this? Do you mean a lack of clarity about what these proposals desugar to? Or something conceptual about what the self keyword is supposed to “be” in these delegation statements/blocks prior to desugaring? I guess it’s a little weird to delegate a static method to self.f, but it seems obvious what it should do regardless.

2 Likes

I agree that I wouldn't want such a transition from "all" to "all but one" to be so catastrophic (I code without an IDE to automate refactoring), though I do also want to be cautious about making the compiler too lenient when it comes to how newcomers might get misled about what's actually going to get called.

As such, only wildcards should support that kind of shadowing. (ie. delegate to self.f for foo; followed by fn foo in the same scope should be an error, just as two fn foo in the same scope is an error.)

That said, as I see it, the issue stems from how "delegate" doesn't carry the connotation of providing default values or declaring a lower layer in a stack of overlays which inheritance does, which is a bigger comprehension problem in Rust than in languages like Python because, in more dynamic languages, it's a simple case of "last declaration wins", just like with variables.

...so I see two options:

  1. Go with delegate or use or what have you and just live with having to teach that "* is special. It just is."
  2. Come up with a word with connotations more like "inherit from" or "fallback to" and accept having to teach that the compiler will only allow shadowing/hiding/overriding a "fallback" if it's pulled in via a wildcard.

I'm partial to the former (just teach that "*" is special) because, to me, it makes sense and shouldn't be difficult to teach that, if you're wildcarding from someone else's interface, them adding a bar function shouldn't cause your wrapper to fail to compile if it already has a bar.