-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
RFC: Delegation #2393
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💯🥇 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.
text/0000-delegation.md
Outdated
|
||
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`: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Formatting nits: Should probably use backticks on S, F, TR.
text/0000-delegation.md
Outdated
|
||
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`: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
text/0000-delegation.md
Outdated
} | ||
``` | ||
|
||
This is pure sugar, and does exactly the same thing as if you “manually delegated” all the items of TR like this: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Formatting nit: backticks on TR
text/0000-delegation.md
Outdated
|
||
``` | ||
|
||
Aside from the implementation of foo(), this has exactly the same meaning as the first example. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Formatting nit: backticks on foo()
text/0000-delegation.md
Outdated
|
||
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
text/0000-delegation.md
Outdated
|
||
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? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
text/0000-delegation.md
Outdated
|
||
- 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? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the initial set is conservative enough 👍
text/0000-delegation.md
Outdated
- 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Formatting nit: backticks on self.
text/0000-delegation.md
Outdated
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
text/0000-delegation.md
Outdated
|
||
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? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would the reason to not allow them be?
In the previous revision:
These questions are considered resolved unless someone objects. |
text/0000-delegation.md
Outdated
|
||
impl AddAssign for AddOnlyCounter { | ||
delegate * to 0; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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();
.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
text/0000-delegation.md
Outdated
|
||
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 wheredelegate * 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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting!
PS: we could leave formatting bikeshed up to a style-fmt RFC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 that's why https://github.com/rust-lang-nursery/fmt-rfcs exists
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 In general, it seems like |
text/0000-delegation.md
Outdated
```rust | ||
fn check_name(&self, name: &str, ignore_capitals: bool, state: &mut State) -> bool { | ||
self.f.check_name(&name, ignore_capitals, &mut state) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 ^,- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
o.O you copied in this verbatim =D
Yes, please (ignoring specific syntax). |
text/0000-delegation.md
Outdated
impl TR for S { | ||
delegate * to field_one; | ||
delegate fn foo, const MAX, type Item | ||
to field_two; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 inS
'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 anF
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
It's possible I missed it, but there's no mention of delegating to nested fields, eg. |
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 |
text/0000-delegation.md
Outdated
```rust | ||
impl TR for S { | ||
fn foo(self: Box<Self>) -> u32 { | ||
self.f.foo() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Do you mean "glob delegation" as in any syntax to delegate everything not explicitly implemented, or just the use of |
@Ixrec the RFC states the shortcoming of
|
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. |
@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 |
@Ixrec We may be able to use |
One other argument in favour of using 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 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. |
Could I delegate 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);
} |
@kornelski No. That probably falls under this item on the list of possible future extensions:
|
Nit: This is proposing a |
A nice compiler error explaining
Since we're delegating to 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 |
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 } |
@mehcode the proc macro would have to have the ability to tell what methods |
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 First Draft: Only methods that don't mention
Umm, |
I've been thinking about this issue more lately, and I've come to the decision that the design outlined by this RFC is probably the best way to go about it. It outlines a much simpler and straightforward solution to the problem than any other proposals I have seen (#493, #2431), although some of those proposals cover more use cases and should not be discounted. This issue comes up a lot from beginners. You explain to them that Rust favors composition over inheritance, and then they get confused as to why there is so much boilerplate. Someone wants to implement an external trait for an external type, and you tell them to new-type it, but then they lose all the other trait implementations and have to resort to copy pasting |
Wanted to help this move so I gave it another read. I agree that this proposal is very good. I'm just not sure about |
@rfcbot fcp postpone Hello everyone; we discussed this RFC in our backlog bonanza. The consensus was that we that we should postpone it, as we don't think we have the bandwidth to see to it right now, but we are very interested in this general direction, and we agree that this addresses some real problems in Rust. We would like to encourage folks to discuss it when the time comes for us to discuss our upcoming roadmap (one of the procedural changes we have in mind is to make it clearer when we'd be open to bigger proposals). Speaking for the team, I do want to apologize. I realize that we encouraged @elahn and others to do this design work, and then let it languish and I'm sorry about that. :( |
Team member @nikomatsakis has proposed to postpone this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! See this document for info about what commands tagged team members can give me. |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
I've checked the names of those folks who were present in the meeting. |
The final comment period, with a disposition to postpone, as per the review above, is now complete. As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed. The RFC is now postponed. |
This is now possible with the |
As far as I can tell, that still has the limitation that the portrait::make macro also needs to be used on the trait definition for that to work. |
Interesting RFC! Such convenient delegation would enhance advantages of composition over inheritance. I wish we'd have this Rust feature someday. P.S. There are crates like delegate, auto-delegate, ambassador that emulate this feature. |
@gamedev-pnc |
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!