There have been many delegation proposals in the past and the consensus is that delegation (forwarding) is a useful feature, but none of the proposals have been formally accepted. Here is my take on it:
Feature: Forwarding
Syntactic sugar for efficient code reuse via the composition pattern. Wrapper functions are generated for a struct, forwarding all, most, or some associated items to a struct member.
Motivation
Take a look at the following snippets of code:
// actix-web/actix-web-actors/src/context.rs
impl<A> ActorContext for HttpContext<A> {
fn stop(&mut self) {
self.inner.stop();
}
fn terminate(&mut self) {
self.inner.terminate()
}
fn state(&self) -> ActorState {
self.inner.state()
}
}
// actix-web/actix-http/src/ws/mask.rs
impl ShortSlice {
fn len(&self) -> usize {
self.inner.len()
}
}
We can see a recurring pattern where the definition of a method is "forwarded" to a struct field. Those are examples of the well known composition pattern. While inheritance derives one class from another, composition defines a class as the sum of its parts. It has a lot of advantages but unfortunately requires writing boilerplate code again and again.
Other non-OOP languages provide forms of efficient code reuse to avoid this boilerplate. For example, Haskell provides {-# GeneralizedNewtypeDeriving #-}
:
newtype NormT m (a :: *) = NormT { _runNormT :: WriterT Unique m a }
deriving ( Eq, Ord, Show, Read, Generic, Typeable
, Functor, Applicative, Monad, MonadFix, MonadIO, MonadZip
, Alternative, MonadPlus, MonadTrans, MFunctor, MMonad
, MonadError e, MonadState s, MonadReader r
, MonadWriter Unique )
Go provides an alternative approach known as "embedding":
type ReadWriter struct {
*Reader
*Writer
}
By providing syntax sugar for method forwarding, non-OOP languages can follow the composition pattern while being as terse as the inheritance-based equivalent.
Guide Level Explanation
In Rust, we prefer composition over inheritance for code reuse. For common cases, we make this convenient with syntax sugar for method forwarding:
impl Trait for Struct {
fn _ => self.field;
}
This is pure sugar, and does exactly the same thing as if you “manually forwarded” all the functions of Trait
like this:
impl Trait for Struct {
fn foo(&self) -> u32 {
self.field.foo()
}
fn bar(&self, x: u32, y: u32, z: u32) -> u32 {
self.field.bar(x, y, z)
}
}
To forward most items of a trait, rather than all of it, simply write the manual implementations for the specific items, and forward the rest:
impl Trait for Struct {
fn _ => self.field;
fn foo(&self) -> u32 {
42
}
}
Aside from the implementation of foo()
, this has exactly the same meaning as the first example.
You can also forward specific functions rather than “all” or “most” items:
impl Trait for Struct {
fn foo, bar => self.field;
}
This also has the exact same meaning as the first example.
Rust also provides a convenient shorthand to forward all associated items to a field:
impl Trait for Struct { _ => self.field; }
You can provide specific implementations as usual:
impl Trait for Struct {
_ => self.field;
type Foo = String;
fn foo() -> String {
// ...
}
}
You can also use forwarding with inherent impl
blocks:
impl Struct {
fn _ => self.field;
fn inherent() {
// ...
}
}
In inherent impl
blocks, forwarding can be combined elegantly with regular method definitions.
Just like with regular inherent methods, forwarded methods are private by default and allow you can specify their visibility:
impl Struct {
pub fn bar => self.field;
pub(crate) fn foo => self.field;
}
Reference-level explanation
A forwarding item can appear inside any impl
block.
Forwarding must be to a field on Self
. Other kinds of implementer expressions are left as future extensions. This also means that forwarding can only be done on structs for now.
Only methods or the entire implementation of a trait can be forwarded.
A forwarding item always consists of:
-
fn
or_
- if
fn
: either a_
, or a comma-separated list of items being forwarded =>
- the forwarding target
self.field_name
- a semicolon
The semantics of a forwarding item should be the same as if the programmer had written each forwarded item implementation manually. For instance, if the Trait
has a default implementation for method foo()
, and the type F
does not provide its own implementation, then forwarding Trait
to F
means using Trait
’s implementation of foo()
. If F
does provide its own implementation, then forwarding Trait
to F
means using F
’s implementation of foo()
. The only additional power granted by this feature is that fn _ =>
can automatically change what items get implemented if the underlying trait Trait
and type F
get changed accordingly.
To generate the wrapper function:
-
The function signature is copied from the function being forwarded to.
-
The self parameter is mapped to the implementer expression
self.field_name
. -
.trait_method_name()
is appended to the implementer expression. -
Subsequent parameters are passed through, e.g.
fn check_name(&self, name: &str, ignore_capitals: bool, state: &mut State) -> bool { self.f.check_name({name}, {ignore_capitals}, {state}) }
It is a compile-time error to forward a trait to a struct field that doesn't implement the trait, or to forward a method to a struct field that does not have that method.
Why this is an improvement over X
There have been many delegation proposals in the past. However, this one solves many of the problems that others ran into:
-
impl TR for S { use self.F; ... }
rules out the possibility ofuse
declarations insideimpl
blocks. -
delegate foo to self.f
requires adding two new keywords.delegate
is also a long (too long?) keyword.
The fn _ =>
syntax is more concise while still being as (or more) readable. It also fits in better with existing Rust syntax:
impl Struct {
fn baz() { ... }
fn foo() { ... }
// define a method `bar` and forward the implementation to `self.field`
fn bar => self.field;
}
Possible Future Extensions
There are a ton of possibilities here. 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 “forwarding” might apply. We do not want to let potential future extensions delay the implementation of a useful language feature. However, it may be useful to discuss these now so that the syntax makes it as easy as possible to extend this feature down the line.
Unresolved Questions
- Should forwarding for associated constants and types be allowed? eg:
const foo => self.field
. I can't see any real use case for it considering that forwarding the entire implementation of a trait is allowed, but maybe it should be allowed for consistency? -
impl Trait for Struct => self.field
instead ofimpl Trait for Struct { _ => self.field; }
? This would remove the functionality of specific implementations for non-method associated types. Arguably that is a very rare case anyways, and writing associated items normally is fine.