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

RFC: Delegation #2393

Open
wants to merge 6 commits into
base: master
from

Conversation

@elahn
Copy link

elahn commented Apr 6, 2018

Syntax sugar for efficient code reuse via the composition pattern. Wrapper functions are generated for a struct, delegating most or all of a trait’s impl block to a member field that already implements the trait.

Rendered

Please Note:

This RFC is a group effort from the Rust community. Whenever an issue is raised, please edit the RFC draft to address it as best you can.

If the design needs to be bikeshedded, please do so on this internals thread.

Whenever an issue or question has been resolved, please submit a PR to this RFC.


Thank you, everyone for your contributions, they’ve been a big help. If we continue this collaborative style throughout the RFC process, I’ve no doubt we can address any concerns that arise and get this puppy accepted!

@Centril Centril added the T-lang label Apr 6, 2018
Copy link
Member

Centril left a comment

💯🥇 to all collaborators on the heroic effort of writing and collaborating on this RFC!

This RFC seems generally well done; I have some formatting nits here and there and some other "more substantive" (not nits) concerns.


In Rust, we prefer composition over inheritence for code reuse. For common cases, we make this convenient with delegation syntax sugar.

Whenever you have a struct S with a member field `f` of type F and F already implements a trait TR, you can delegate the implementation of TR for S to `f` using the contextual keyword `delegate`:

This comment has been minimized.

Copy link
@Centril

Centril Apr 6, 2018

Member

Formatting nits: Should probably use backticks on S, F, TR.


In Rust, we prefer composition over inheritence for code reuse. For common cases, we make this convenient with delegation syntax sugar.

Whenever you have a struct S with a member field `f` of type F and F already implements a trait TR, you can delegate the implementation of TR for S to `f` using the contextual keyword `delegate`:

This comment has been minimized.

Copy link
@Centril

Centril Apr 6, 2018

Member

We probably want to make delegate a real keyword due to current (one day old) lang team keyword policy that new features should be real keywords for maintenance reasons.

Here is a quick review of the breakage risk:

  • TL;DR: The risk is quite minimal and something we could probably live with.

  • Usage as ident in libstd: No

  • Usage as the name of a crate: No

  • Usage as idents in crates (sourcegraph): 19+ uses

}
```

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

This comment has been minimized.

Copy link
@Centril

Centril Apr 6, 2018

Member

Formatting nit: backticks on TR

```

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

This comment has been minimized.

Copy link
@Centril

Centril Apr 6, 2018

Member

Formatting nit: backticks on foo()


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.

This comment has been minimized.

Copy link
@Centril

Centril Apr 6, 2018

Member

A good default here could be that fn is assumed (since it is most common), and so you could instead write:

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

another possible shorthand notation could be (but I am not proposing it at this point):

impl TR for S {
    delegate
        fn { foo, bar, the_hulk, black_widdow },
        const { MAX, MIX },
        type { Item, Baz, Bar }
        to f;
}

but you are allowed to prefix with fn if you so wish.


We expect to resolve through the RFC process before this gets merged:

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

This comment has been minimized.

Copy link
@Centril

Centril Apr 6, 2018

Member

My gut feeling is that it is feasible. delegate <stuff> to <field>; should be considered an item, and as such, you'd [rough sketch] first do a pass collecting all the items and after that you simply extract the delegate items and desugar those into a flat set, remove all the idents (LHS) found in the remaining set from the flat set, and then you finally merge the sets.

I think rustfmt should put them at the top, but people should be able to do as they like and the grammar should be flexible enough to accommodate that if it is technically possible.


- 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?

This comment has been minimized.

Copy link
@Centril

Centril Apr 6, 2018

Member

I think the initial set is conservative enough 👍

- 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?
- Although the syntax and desugaring for "delegating some methods to one field and some to another" is straightforward, should it be postponed as a possible future extension?
- Are there any cases of [Custom Self Types](https://github.com/rust-lang/rfcs/pull/2362) where self needs to be manually dereferenced, e.g.

This comment has been minimized.

Copy link
@Centril

Centril Apr 6, 2018

Member

Formatting nit: backticks on self.

If so, can these be handled during implementation of this feature or is upfront design work required?
- There is a concern about _inherent traits_ causing duplicated symbols, can this be resolved during implementation?
- Is the possible future extension _delegate block_ ruled out? If not, keywords `delegate`/`Delegate` should be reserved in edition 2018.
- Should we implement the proposed syntax or one of the alternatives in nightly? We may wish to gain experience using a particlular syntax on nightly before committing to it.

This comment has been minimized.

Copy link
@Centril

Centril Apr 6, 2018

Member

That could be done concurrently with the RFC if someone has the time, but I don't think an experimental RFC is necessary here, the current design is pretty good.


We expect to resolve through the implementation of this feature before stabilization:

- How does delegation interact with specialization? There will be a [default impl](https://github.com/rust-lang/rfcs/blob/master/text/1210-impl-specialization.md#default-impls) block in the future. Should we allow `delegate` to be used in a `default impl` block?

This comment has been minimized.

Copy link
@Centril

Centril Apr 6, 2018

Member

What would the reason to not allow them be?

@elahn

This comment has been minimized.

Copy link
Author

elahn commented Apr 6, 2018

In the previous revision:

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.

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

@Centril: My gut feeling is that it is feasible. delegate to ; should be considered an item, and as such, you'd [rough sketch] first do a pass collecting all the items and after that you simply extract the delegate items and desugar those into a flat set, remove all the idents (LHS) found in the remaining set from the flat set, and then you finally merge the sets.

I think rustfmt should put them at the top, but people should be able to do as they like and the grammar should be flexible enough to accommodate that if it is technically possible.

@Ixrec: Based on my memory of past and current delegation threads:

  1. Some people thought this was required to make the contextual keyword option viable. I think this line of reasoning predates epochs/editions. It's obviously obsolete now that we have official guidance on new keyword introduction.
  2. When an impl block contains a delegation as well as a regular item implementation, it seems better style to put the delegation on top. Especially when it's a * delegation, since "Foo implements trait T by delegating almost everything to field f, except that method foo is ... " is the natural way to describe that sort of thing.

So imo, as part of editing this to say delegate should be a real keyword, we should also delete this restriction. Putting the delegate item(s) at the top is probably still good style, but there's no need to make it mandatory.

Unresolved question: 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?

@Centril: I think the initial set is conservative enough 👍

These questions are considered resolved unless someone objects.

impl AddAssign for AddOnlyCounter {
delegate * to 0;
}

This comment has been minimized.

Copy link
@scottmcm

scottmcm Apr 7, 2018

Member

I was already thinking of suggesting that it be delegate <list> to self.<field>, and this solidified the feeling. (delegate fn hash to 0 looks like it'd do 0.hash() to me.) I like there being restrictions as a starting point, but I wouldn't be surprised at all for this to get expanded, and expanding to expressions (perhaps a subset thereof, but note that unimplemented!() would just work if it took anything) seems like the most obvious one, so I think the syntax should at least look like a normal expression from the beginning. (Readers: feel free to 👍(👎) if you would(n't) like me to make a PR for that.)

Edit: Oh, I see the mention of this in the alternatives. I'm not convinced learnability is a big problem; even with just a field name it seems like it's "oh, they'll put self. in front" and expect more things to work, and it'd be easy to give a very clear error for "delegation only supports a single field". More abstractly, is there a reason it couldn't support an arbitrary expression? There's already a "implementer expression" concept in the reference section below. (Well, ones where let _ = expr; could infer the type, giving the same "cannot infer type for T" errors you get from things like that let if you tried to do something like delegate * to self.0.into();.)

This comment has been minimized.

Copy link
@Centril

Centril Apr 7, 2018

Member

Personally, I'm quite torn on the subject of self vs. not. While self.field is clearer, it is also a bit more verbose; and you could support arbitrary expressions with a { } enclosing of { unimplemented!() }.
With respect to error messages and learnability I agree that this isn't a big concern.


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.

There may be more than one delegation statement. For readability, `rustfmt` moves delegation statements to the top of an impl block.

This comment has been minimized.

Copy link
@scottmcm

scottmcm Apr 7, 2018

Member

Perhaps justify why this is more readable? I can see an argument for *, but especially for single-method delegation, I'm not convinced. Particularly in a case where I'm changing existing code to replace manual delegation with delegate, I'd expect rustfmt to leave the the item where it is so the diff is the obvious one. And even in new code, I might be intentionally matching the order of the methods on the trait, for example. (Nit-picky: if it's going to move multiple, it would need to pick an order to put them in.)

This comment has been minimized.

Copy link
@Centril

Centril Apr 7, 2018

Member

My current thinking here is that:

  • The argument starts from *, which should be at the top so that it is seen first
  • For consistency with *, you place all delegation where delegate * to whatever would be

An argument could however be made that all delegations should be at the bottom since they often will be delegate * and so they are a sort of "catch the rest", i.e, they function like match x { important => .. , _ => .. } does wrt. _ =>.

With respect to order, I'd first group items by item type and then in each group alphabetically so:

  • const
  • type
  • fn

This comment has been minimized.

Copy link
@scottmcm

scottmcm Apr 8, 2018

Member

BTW, does rustfmt reorder any other items? I tried on play and it doesn't seem to reorder type and fn in an impl block, for example...

This comment has been minimized.

Copy link
@Centril

Centril Apr 8, 2018

Member

Interesting!

PS: we could leave formatting bikeshed up to a style-fmt RFC.

This comment has been minimized.

Copy link
@scottmcm
@scottmcm

This comment has been minimized.

Copy link
Member

scottmcm commented Apr 7, 2018

What happens when things go wrong? For example, what happens if I try to

impl Default for AddOnlyCounter {
    delegate * to 0;
}

Is there a way to map output (and output types)? For example, if I do

impl Add for AddOnlyCounter {
    delegate * to 0;
}

It seems like the output type is most likely u32, but I'd probably want it to be AddOnlyCounter. And what's the RHS? Does it just fail because the impl wants the RHS to be AddOnlyCounter, but self.0.add wants a u32?

In general, it seems like Self anywhere that's not also self is a landmine here.

```rust
fn check_name(&self, name: &str, ignore_capitals: bool, state: &mut State) -> bool {
self.f.check_name(&name, ignore_capitals, &mut state)
}

This comment has been minimized.

Copy link
@scottmcm

scottmcm Apr 7, 2018

Member

I don't think I know what "according to their type" means here. Why are the extra &s needed, instead of just moving them all? Perhaps self.f.check_name({name}, {ignore_capitals}, {state}).

, MonadWriter Unique )
```

This is massive code reuse and not in any OOP language ^,-

This comment has been minimized.

Copy link
@Centril

Centril Apr 7, 2018

Member

o.O you copied in this verbatim =D

@petrochenkov

This comment has been minimized.

Copy link
Contributor

petrochenkov commented Apr 7, 2018

Yes, please (ignoring specific syntax).
This has potential to remove more useless boilerplate than all the "ergonomic initiative" RFCs combined.

impl TR for S {
delegate * to field_one;
delegate fn foo, const MAX, type Item
to field_two;

This comment has been minimized.

Copy link
@tanriol

tanriol Apr 7, 2018

Would be nice to have a bit more motivation for this. Treating a trait as a unit of behavior, in what situations does it make sense to delegate some behavior to one field and some to a different one?


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 authors' rationale for preferring `delegate ... to field_name;` over all of these alternatives.

- `impl TR for S use self.F { ... }` was criticized in the first RFC’s comment thread for looking too much like inheritance.

This comment has been minimized.

Copy link
@jan-hudec

jan-hudec Apr 7, 2018

Why is that a problem? It basically is inheritance. Or a restricted version of it, because inheritance delegates all methods to the designated member called “base” while this only delegates methods of specific trait. But that restriction is obvious from this syntax.

This comment has been minimized.

Copy link
@Ixrec

Ixrec Apr 7, 2018

Contributor

"Inheritance" traditionally means a LOT more than simply delegating implementations to another type. For instance: committing to the same interface, having the same internal structure as another type, or being implicitly convertable to other types. In Rust those things are usually kept separate, which we'd like to keep doing.

So if we wanted to make this part of the RFC clearer, we could say that impl TR for S use self.F {} makes it surprising that a field named F must already exist in S's declaration, rather than being implicitly added by this impl TR for S use self.F {}. Or we could say that impl TR for S use self.F {} makes it surprising that S cannot be implicitly converted to an F. And so on.

I don't personally think there's any one or two specific features in the "inheritance" grabbag that most users would mistakently expect when they see impl TR for S use self.F {}, but I do think a lot of people would mistakenly expect something on top of method delegation if they saw that syntax. It might be that "it looks too much like inheritance" is the only concise way to present that argument without getting into the weeds of what people think "inheritance" means.

This comment has been minimized.

Copy link
@jan-hudec

jan-hudec Apr 8, 2018

Inheritance does not mean all that much more than delegation, really. It does mean that all interfaces will be delegated, but it should be obvious it's not happening here, because it starts by stating which trait it is delegating. It does not mean committing to the same structure, only to containing that structure, but that needs to happen here as well. The only other thing inheritance does is the implicit coercion of reference. That won't happen here, but we only said we are delegating specific trait after all. And coercion to that trait does work.

This form would not be appropriate for delegating inherent methods, because that would look like it might be doing more than it does. But for the traits, it would be convenient shortcut that does not really look like promising more than it does.

makes it surprising that a field named F must already exist in S's declaration

Since it does not mention the type of F, it's quite clear that that still has to be given.

makes it surprising that S cannot be implicitly converted to an F

I would somewhat agree for inherent methods, i.e. impl S use self.F {}. But I wouldn't fear that much for the impl TR for S case.

This comment has been minimized.

Copy link
@jan-hudec

jan-hudec Apr 8, 2018

Hm, but there is one difference that I have to admit would be quite confusing—in case of inheritance, the overridden behaviour applies even if you've got a reference to the “base field”. But in our case it won't. In fact, I fear delegating some methods, but not all of them, will be confusing with any syntax.

@Diggsey

This comment has been minimized.

Copy link
Contributor

Diggsey commented Apr 8, 2018

It's possible I missed it, but there's no mention of delegating to nested fields, eg. delegate * to x.y - it seems... odd to constrain it to a direct field.

@WiSaGaN

This comment has been minimized.

Copy link
Contributor

WiSaGaN commented Apr 8, 2018

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.

I still prefer this syntax proposed by @eddyb . This syntax does not introduce new keyword (contextual or not). And the meaning is obvious, and "feels" just right. I would argue that we should not encourage "glob" delegations. I am wondering how many cases are left for "glob" if we can use impl MyTrait for MyType => self.0; to delegate all methods in a trait.

```rust
impl TR for S {
fn foo(self: Box<Self>) -> u32 {
self.f.foo()

This comment has been minimized.

Copy link
@Nemo157

Nemo157 Apr 8, 2018

Contributor

I don’t believe the linked Custom Self Types RFC adds support for implicit projections of smart pointers (I don’t think it is possible in general, given an Rc<T> you can’t obtain an Rc<SomeFieldOfT>), which would be necessary for this to work.

@Ixrec

This comment has been minimized.

Copy link
Contributor

Ixrec commented Apr 8, 2018

@WiSaGaN

I would argue that we should not encourage "glob" delegations.

Do you mean "glob delegation" as in any syntax to delegate everything not explicitly implemented, or just the use of * in the proposed syntax? I would've thought "glob delegation" meant the former, but you appear to be proposing that impl MyTrait for MyType => self.0; do exactly the same thing in the very next sentence.

@WiSaGaN

This comment has been minimized.

Copy link
Contributor

WiSaGaN commented Apr 8, 2018

@Ixrec the RFC states the shortcoming of fn method = self.field.method; syntax is the inability to handle multiple, most or all items in an impl. I assume this means the cases not covered by impl MyTrait for MyType => self.0;. In these cases, explicitly spelling out the functions one need instead of conveniently pulling in all functions not grouped by any traits seems desirable because it

  • promotes conservatively exposing functions in the new type that reduces the possibility of accidentlly pulling in unintended functions
  • minimizes reader/maintainer's search effort for what are delegated exactly
@leoyvens

This comment has been minimized.

Copy link

leoyvens commented Apr 8, 2018

I'm happy that there are people working on this as I find the motivation compelling. But one of the reasons #1406 was closed was that it tried to do too much, with too much syntax. I think this RFC still has that same problem. Personally the only feature I want is to delegate an entire trait implementation to a field. An attribute would do fine, I see no need for first-class syntax.

@Ixrec

This comment has been minimized.

Copy link
Contributor

Ixrec commented Apr 8, 2018

@leodasvacas To clarify, are you only talking about the introduction of dedicated syntax, or do you also feel this RFC "does too much"? It sounds like you're saying both, but the actual functionality proposed here (i.e., ignoring all of the possible future extensions) is a small subset of what #1406 proposed and under-specified. And part of the reason #1406 was closed is because its proposed reuse of an existing keyword was potentially confusing and ambiguous; now that we have epochs/editions introducing a new keyword is intended to be an improvement on it (if you have a specific alternative to a new keyword without the problems of #1406 I'd love to hear it).

@WiSaGaN I honestly can't figure out what you're trying to say in that comment. Are you objecting to the feature of delegating all items? Or the feature of delegating most/all non-explicitly implemented items? Or some specific syntax for either or both of those? I believe impl MyTrait for MyType => self.0; syntax can handle the all case but not the most case, while I don't see how fn method = self.field.method; could handle either.

@WiSaGaN

This comment has been minimized.

Copy link
Contributor

WiSaGaN commented Apr 9, 2018

@Ixrec We may be able to use impl MyTrait for MyType => self.0; with fn method = self.field.method;. These two won't cover the case where a type has many inherent methods, and you want to delegate all of them in one line, and the "most" cases. By using fn method = self.field.method; in these cases, you typed more, but it has the two benefits listed in my previous comment.
This approach though, does not introduce "delegate", nor it introduces the "*".

@clarfon

This comment has been minimized.

Copy link
Contributor

clarfon commented Apr 10, 2018

One other argument in favour of using self.field instead of field is this:

impl<'a, T: 'a + Trait> Trait for &'a T {
    delegate * to *self;
}

Which, IMHO, should be allowed from the start. So many traits do this and I think it's extremely reasonable to include it. Additionally, disallowing delegate * to self at least when deny(unconditional_recursion) is on is also super important; personally, I'd even say that this should be a hard error.

I think that it should be clarified that this syntax should not alter the behaviour compared to the manual equivalent. To the point where, if they differ, it should be considered a compiler bug. This goes either way; if something works with delegation (as it should) when it fails manually, I'd consider it a bug.

@kornelski

This comment has been minimized.

Copy link
Contributor

kornelski commented Apr 10, 2018

Could I delegate Ord to a combination of two fields?

struct MyStruct {
   field_a_ignore_me: Foo,
   field_b: u8,
   field_c: String,
}

impl Ord for MyStruct {
    delegate * to |&self| (self.field_b, self.field_c);
}
@Ixrec

This comment has been minimized.

Copy link
Contributor

Ixrec commented Apr 10, 2018

@kornelski No. That probably falls under this item on the list of possible future extensions:

Delegating “multiple Self arguments” for traits like PartialOrd, so that delegate * to f; would desugar to something like self.f.partial_cmp(other.f)

@Havvy

This comment has been minimized.

Copy link
Contributor

Havvy commented Apr 11, 2018

Nit: This is proposing a delegate associated item, not a statement.

@elahn

This comment has been minimized.

Copy link
Author

elahn commented Apr 12, 2018

@scottmcm: What happens when things go wrong? For example, what happens if I try to

impl Default for AddOnlyCounter {
    delegate * to 0;
}

A nice compiler error explaining Default isn't implemented for u32, so the implementation for items of Default cannot be delegated to AddOnlyCounter::0.

Is there a way to map output (and output types)? For example, if I do

impl Add for AddOnlyCounter {
    delegate * to 0;
}

It seems like the output type is most likely u32, but I'd probably want it to be AddOnlyCounter. And what's the RHS? Does it just fail because the impl wants the RHS to be AddOnlyCounter, but self.0.add wants a u32?

Since we're delegating to u32, the signature of u32.add() is used, so RHS is u32:

impl Add for AddOnlyCounter {
    type Output = <u32 as Add>::Output;

    fn add(self, other: u32) -> u32 { self.0.add(other) }
}

To map the output type using the possible future extension "delegate block":

impl Add for AddOnlyCounter {
    type Output = AddOnlyCounter;
    delegate * {
        |self| self.0
    } -> {
        |delegate| AddOnlyCounter { delegate }
    }
}

// Generated code:
impl Add for AddOnlyCounter {
    type Output = AddOnlyCounter;

    fn add(self, other: u32) -> AddOnlyCounter {
        AddOnlyCounter { self.0.add(other) }
    }
}

For RHS to be AddOnlyCounter, the possible future extension "delegating multiple Self arguments" would be needed.

@mehcode

This comment has been minimized.

Copy link

mehcode commented Apr 16, 2018

I think I'm missing the point here. Couldn't this be trivially provided by a crate and a proc macro for the common case? Does it really need to be syntax?

#[delegates(Trait, "bar")]
struct Foo { bar: Bar }
@clarfon

This comment has been minimized.

Copy link
Contributor

clarfon commented Apr 17, 2018

@mehcode the proc macro would have to have the ability to tell what methods Trait has, so, it'd be extremely limited.

@scottmcm

This comment has been minimized.

Copy link
Member

scottmcm commented Apr 19, 2018

Meta-comment: I definitely want some form of delegation, but I think this RFC could still do a better job of carving out a simple and uncontroversial subset. For example, when I think of delegating Add for struct MyNewtype(u32), I expect MyNewtype: Add<MyNewtype, Output=MyNewtype>. This RFC doesn't do that. And that's fine; I'd just rather, then, that it was a "isn't supported for delegation at this time" error. Maybe a simple rule can be found for "delegation-safe" items (like there's a rule for "dyn-compatible traits) that this can be restricted to for now, with additional support added later for the tricky cases.

First Draft: Only methods that don't mention Self other than in self, &self, and &mut self.

A nice compiler error explaining Default isn't implemented for u32

Umm, u32 is Default: https://doc.rust-lang.org/beta/std/primitive.u32.html#impl-Default

@burdges

This comment has been minimized.

Copy link

burdges commented Dec 13, 2018

I love the proposal, except..

delegate fn foo use self.0 saves making to into a keyword. Just imagine it as two imperatives as opposed to an imperative with a clause.

All these "items" should use ; over , so

impl Trait {
    delegate {
        fn foo;
        type BAR;
        const BAZ;
    } use self.meow;
    fn purr() { .. }
}

and the freestanding

delegate Trait {
    fn foo;
    type BAR;
    const BAZ;
} use {
    |&self| self.get_inner();
    |&mut self| self.get_inner_mut();
    |self| self.into_inner();
    |x: Rc<Self>| self.rc_into_inner_rc();
} -> {
    |delegate| Self::from_inner(delegate);
    |x: Rc<Delegate>| Self::rc_from_inner_rc(x);
}

I wrote ; in the multi-receiver type trick here too because they do not really "accompany one another" in the way that fields or variants do. I'd judge them to more similar to items than to variants, but obviously if you consider them more similar to variants then you'd probably prefer , instead.

We should think more about this multi-receiver type business too. Is this "macro-like reciver polymorphism" sensible elsewhere? I think so, although not always advisable. If we want it elsewhere, then is delegation a sensible mechanism for doing this everywhere? Again yes maybe, but we should discuss alternatives too. In particular if we had methods/functions that did this then we could delegate to an inherent method, and do the "macro-like reciver polymorphism" there.

@alexreg

This comment has been minimized.

Copy link

alexreg commented Dec 17, 2018

While I think the motivation and intention for this feature are good, I am kind of worried about what happens when you want to change something from a normal fn (that may be delegation plus some minor logic) to simple delegation, and vice versa. The syntaxes are completing the different. This is essentially the reason why if-statements without curly brackets have typically been proscribed in C-like languages, and the pain point here is even worse I reckon.

@mark-i-m

This comment has been minimized.

Copy link
Member

mark-i-m commented Dec 17, 2018

@alexreg Are you concerned about ergonomics or about diff size? Could you give an example of a problematic case?

@alexreg

This comment has been minimized.

Copy link

alexreg commented Dec 17, 2018

@mark-i-m Ergonomics.

@shepmaster

This comment has been minimized.

Copy link
Member

shepmaster commented Dec 18, 2018

when you want to change something from a normal fn (that may be delegation plus some minor logic) to simple delegation, and vice versa. The syntaxes are completing the different

This does not concern me. Some examples from Ruby, where I use delegation relatively frequently. I've picked the two variants I see the most and squashed the code around a bit to make them comparable:

class Example
  def initialize(sandwich_maker)
    @sandwich_maker = sandwich_maker
  end
  
   ## By hand

  def make_me_a_sandwich
    sandwich_maker.make_me_a_sandwich
  end

  ## Forwardable

  extend Forwardable  
  def_delegators :@sandwich_maker, :make_me_a_sandwich

  ## ActiveSupport

  require 'active_support/core_ext/module/delegation'

  attr_reader :sandwich_maker
  delegate :make_me_a_sandwich, to: :sandwich_maker  
end

On the flip side, I think I'd prefer visually distinct versions because they catch my eye and let me know that "nothing special is going on here, just forwarding the call, thanks".

Ergonomics-wise, I think the big thing is that delegating a single method needs to be shorter than writing it out. A basic Rust function is going to be 3 lines, so ideally we can opt-in to delegation with zero or one line of overhead (the extend Forwardable / require 'active_support/core_ext/module/delegation' equivalent from the Ruby examples) and then perform the delegation with one more line per delegated function.

@shepmaster

This comment has been minimized.

Copy link
Member

shepmaster commented Dec 18, 2018

This also makes me realize that I'd like to see some discussion of renaming. A shortened example from this years Advent of Code:

struct Game {
    board: Board,
    last_board: Board,
}

impl Game {
    fn pieces_left(&self) -> usize {
        self.last_board.pieces()
    }
}

struct Board(Vec<u8>);

impl Board {
    fn pieces(&self) -> usize {
        self.0.len()
    }
}

I'd like to see this supported (using some strawman syntax):

struct Game {
    board: Board,
    last_board: Board,
}

impl Game {
    delegate pieces to self.last_board as pieces_left;
}

struct Board(Vec<u8>);

impl Board {
    delegate len to self.0 as pieces;
}
@alexreg

This comment has been minimized.

Copy link

alexreg commented Dec 18, 2018

Ergonomics-wise, I think the big thing is that delegating a single method needs to be shorter than writing it out. A basic Rust function is going to be 3 lines, so ideally we can opt-in to delegation with zero or one line of overhead

Fair enough, but I'd at least like to see more similar syntax. The difference shouldn't be so blatant, for the reasons of both false expectations and moreover ergonomics.

@shepmaster

This comment has been minimized.

Copy link
Member

shepmaster commented Dec 18, 2018

for the reasons of both false expectations and moreover ergonomics.

I don't follow what you mean by either "false expectations" or "ergonomics" in this case. Could you provide concrete examples of what each means?

@clarfon

This comment has been minimized.

Copy link
Contributor

clarfon commented Dec 18, 2018

Why not use symbols instead of words? i.e. delegate (self, pieces_left) = (self.board, pieces)

@alexreg

This comment has been minimized.

Copy link

alexreg commented Dec 18, 2018

I don't follow what you mean by either "false expectations" or "ergonomics" in this case. Could you provide concrete examples of what each means?

For ergonomics:

I'd like to see a syntax somewhat like this used for delegation, if we decide to support it at all:

impl<'a> Hash for H<'a> {
    fn hash<H: Hasher>(&self, state: &mut H) {
		delegate_to!(self.name);
	}
}

It would be nice for this to be a proc macro even, rather than a language extension.

By "false expectations", I just mean that the syntax proposed in the RFC is far away from the usual function definition syntax, so not immediately obvious a function is being defined.

Overall though, I'm still not convinced that the utility feature adds to the language outweighs the expanded surface area/complexity of syntax.

@olson-sean-k

This comment has been minimized.

Copy link

olson-sean-k commented Dec 19, 2018

It would be nice for this to be a proc macro even, rather than a language extension.

I think a language extension could be very useful for this. Repeating functions can be extremely burdensome, even if there are just a handful of them. That kind of friction makes anti-patterns like the Deref hack seem attractive relatively quickly IMO.

In an extreme example, I have a crate with types that nest four levels deep in order to take ownership of different data and prevent clients from performing arbitrary mutations. A single top-level type that is part of the public API then delegates to a field and so on.

In this case, delegating using macros still leads to 100s of lines of code, as delegation (re)-occurs at each level and each level exposes as many as 10 functions. I'm hoping for a way to manage this that avoids anti-patterns like the Deref hack and excessive code duplication.

@alexreg

This comment has been minimized.

Copy link

alexreg commented Dec 19, 2018

You still ignore the issue of ergonomics though.

@shepmaster

This comment has been minimized.

Copy link
Member

shepmaster commented Dec 19, 2018

It would be nice for this to be a proc macro even, rather than a language extension.

Many people have tried to do this (including myself) since probably before Rust 1.0. It cannot be done in a macro because a macro does not have access to the arguments / types of an arbitrary type or trait. That is, a macro cannot say delegate!(Iterator, self.inner) and magically know that Iterator has a next method or the argument counts / types or the return type.

I'd like to see a syntax somewhat like this

You still ignore the issue of ergonomics

I'd claim that your suggested syntax is the one that ignores ergonomics, as it means I need to know the types of the function's arguments/return value; to me, being able to ignore that is a huge point of delegation.

the syntax proposed in the RFC is far away from the usual function definition syntax, so not immediately obvious a function is being defined.

This is true, but I'm OK with calling something delegation (or one of the synonyms discussed upthread) and expecting a programmer to know what "delegation" means or to look it up.

A semi-related example is implementing a trait:

impl Iterator for MyType {
    type Item = ();
    fn next(&mut self) -> Option<()> { None }
}

There are zero characters here that indicate that the functions count or map are available on MyType, but we accept that. Delegation syntax is more explicit by stating what functions / traits are being created.

A weird in-between form of the strawman explicit delegation syntax applied to the "magically occurring iterator functions" could be:

impl Iterator for MyType {
    type Item = ();
    fn next(&mut self) -> Option<()> { None }

    fn count;
    fn map;
    // etc...
}

But I think people would balk at this, for "ergonomic" reasons.

@alexreg

This comment has been minimized.

Copy link

alexreg commented Dec 19, 2018

You make a fair point... when I said "ergonomics" above, however, I meant the ergonomics of switching between a delegated method and a normal one. It involves completely removing one line and adding more lines. The function header syntax isn't shared, or such. That's what I'm getting at. I suppose some sacrifice has to be made for the ergonomics of writing such delegations in the first place though...

@jan-hudec

This comment has been minimized.

Copy link

jan-hudec commented Dec 27, 2018

@alexreg, I don't think that's the right kind of ergonomics in this case though. Delegation is useful when you can delegate a lot of methods to a member with very little typing. Once you have to write out the signatures, there is nothing to be saved any more and the feature becomes useless.

Compare

impl<'a> Hash for H<'a> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        delegate_to!(self.name);
    }
}

with the current code

impl<'a> Hash for H<'a> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.name.hash(state)
    }
}

What do you save with the former, again?

Ergonomic in this case means easy to delegate a lot of things with little typing. This is most ergonomic:

struct S {
    #[delegate(Trait, OtherTrait)]
    f: Type
}

with this being almost as good:

#[delegate_to(f: Trait, OtherTrait)]
struct S {
    f: Type
}

Remember, the Haskell equivalent is just generalized deriving. Even this is more explicit than the Haskell version stating to which field we are delegating—Haskell simply delegates to whichever field is of type that is in requested type class.

The syntax in the current proposal,

impl Trait for S { delegate * to self.f }
impl OtherTrait for S { delegate * to self.f }

is really on the edge of being too verbose (can be shortened with a macro). Anything more verbose severely reduces the usefulness of the feature.

@burdges

This comment has been minimized.

Copy link

burdges commented Dec 28, 2018

We could use an unstable variant of the syntax in this RFC to provide a stable field oriented syntax, ala

struct S {
    #[delegate(Trait, OtherTrait)]
    f: Type
}

There are however several iffy RFCs like this that exist primarily because proc macros cannot really access type level information. We might therefore investigate ways for proc macros to delay themselves until type checking begins and then query the type checker. I'd worry doing that might hurt compile times, but it might yield a far cleaner language.

@alexreg

This comment has been minimized.

Copy link

alexreg commented Dec 28, 2018

@jan-hudec You (and others) are starting to win me over... let's see where this goes.

@burdges

This comment has been minimized.

Copy link

burdges commented Jan 1, 2019

An interesting trick for a custom test framework #2318 would be verifying that some specializations rust-lang/rust#31844 produced the same results as less specialized impls. I think delegations are about the only way I can imagine doing this nicely.

@burdges burdges mentioned this pull request Jul 16, 2019
@YairHalberstadt

This comment has been minimized.

Copy link

YairHalberstadt commented Jul 21, 2019

I was looking to see if a mixin feature was RFCed and nothing was showing up on google. This looks like what I was looking for, so I'm just using the word mixin here so that it shows up more easily in google searches.

This would be a great feature!

@jan-hudec

This comment has been minimized.

Copy link

jan-hudec commented Aug 5, 2019

@YairHalberstadt, I am not sure what you mean by “mixin”, but what e.g. Ruby means has been in Rust from the very beginning, because it is just traits with inherent methods.

@hobofan

This comment has been minimized.

Copy link

hobofan commented Nov 19, 2019

For anyone interested in this, I published a crate which does (parts of) this: ambassador.

It circumvents the problem of "we cannot know the methods of a trait, and their argument counts", by requiring a second proc_macro to be used on the trait, which generates a macro_rules! macro, which contains the actual delegation mechanism. This was just recently enabled by stabilizing rust-lang/rust#64035. For foreign traits, there is also a mechanism to do the same thing.

Obviously still early days for this approach, but it looks like quite a bit more out-of-tree experiments are now possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.