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?