Pre-RFC: Forwarding

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 of use declarations inside impl 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 of impl 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.
8 Likes

How will you document forwarded items?

Can you delegate methods that take ownership of self?

Does Self in delegated methods refer to the implementer or the delegate? How does this work if the inner type is private?

Can you delegate to trait methods?

Can you delegate to fields of fields?

How useful is this if you can only delegate to fields posessing identical method names? For instance, if you can't impl Display by pointing at impl Debug.

What happens if you delegate to a field from another crate and they've changed their API? Do you silently export the new API as well?

One problem that these delegation RFCs tend to have is that they compete with copy+paste/find+replace, which takes only a few seconds to do and opens all of the doors to customizing your implementation however you see fit, rather than trying to enable features through some obscure and indirect syntax.

4 Likes

This seems simple enough - have the documentation for the inner struct be inlined, the same way documentation for re-exports is inlined.

3 Likes

Can you delegate methods that take ownership of self ?

Yes, why would that be any different than functions that take &self ?

How does this work if the inner type is private?

Exactly the same:

pub struct Blah(Inner);

impl Blah {
  fn foo => self.inner;
}

Can you delegate to trait methods?

This RFC does not define a way to do that. That could be a potential future extension.

How useful is this if you can only delegate to fields posessing identical method names? For instance, if you can't impl Display by pointing at impl Debug .

Very useful. There are a million things that could be added here, which would fall under the category of "future extensions" (I'll add a note about this in the text). I'm sure that none of use want to let potential future extensions delay the implementation of a useful language feature.

Does Self in delegated methods refer to the implementer or the delegate?

self in a delegated method refers to the implementor. Remember, this is just syntactic sugar for forwarding:

impl Trait for Struct {
    fn bar => self.field;
}

becomes:

impl Trait for Struct {
    fn bar(&self) -> u32 {
        self.field.bar()
    }
}
4 Likes

That's the easy solution, but if I click your docs for a method on struct A and they point me at struct B, I'll consider your code substandard.

Because taking ownership of a struct and only consuming one field is almost never what you want to do.

That's the easy solution, but if I click your docs for a method on struct A and they point me at struct B , I'll consider your code substandard.

That is not how inlining works. For example, std::iter::Iterator - Rust is actually defined in core, but the documentation appears just as if the item were defined in std.

4 Likes

But it's called Iterator in both core and in std. It hasn't been grafted onto a new type during that process. People would be understandably confused if they look up on docs for StdIterator and end up at CoreIterator, when they are designed to be entirely different things.

(And this says nothing about how well that documentation would reflect semantic changes, such as when you call a method that drops an entire wrapper struct, rather than just the delegated field.)

I quite like this proposal. I do have one question:

Would visibility modifiers be usable with the "wildcard", and if so how would they behave? In other words would:

impl Trait for Struct
    pub(crate) fn _ => self.field;
}

be allowed?

If so would it match functions only with the same visibility, or would it match functions at that visbility, or above? So would fn _ => self.field delegate all private functions or would it also include, for instance, pub functions?

Well, your example would not be allowed because you cannot specify the visibility of trait methods. With regards to the broader question, I haven't documented this, but I would say that fn _ => self.field forwards all methods of self.field that are visible to Struct. Saying pub(crate) or pub simply means that the generated methods will be pub(crate) or pub.

For example:

struct Inner;

impl Inner {
  pub inner_pub(&self) {}
  inner_priv(&self) {}
}

struct Blah(Inner);

impl Blah {
  fn _ => self.inner;
}

expands to:

impl Blah {
  inner_pub(&self) { self.0.inner_pub() }
  inner_priv(&self) { self.0.inner_priv() }
}

and:

impl Blah {
  pub(crate) fn _ => self.inner;
}

expands to:

struct Blah(Inner);

impl Blah {
  pub(crate) inner_pub(&self) { self.0.inner_pub() }
  pub(crate) inner_priv(&self) { self.0.inner_priv() }
}

That said, there could be future extensions that would allow you to say, "forward all private methods of self.field", but I think that is out of the scope of this RFC.

What happens if I am using a crate providing

impl A {
    pub fn one(&self) {}
}

and I write

impl B {
    fn _ => self.a;
    fn two(&self) -> Option<()> { None }
}

and then the crate does a non-breaking change to:

impl A {
    pub fn one(&self) {}
    pub fn two(&self) {}
}
2 Likes

I think then the new fn two of the inner type would just not be forwarded.

However, IMHO that part of this RFC has the least motivation so far and should maybe be moved to future possibilities. Forwarding specific inherent methods while explicitly choosing their visibility seems like a good idea, I'm not so sure about re-exporting all visible methods (publicly, or even with pub(crate)). For forwarding arbitrary method calls to a field, we already have Deref and DerefMut, this wouldn't really improve on that.

1 Like

What does forwarding inherent methods with fn _ => self.field; do with methods and/or associated functions that cannot be forwarded (for example, fn new() -> Self or fn wrap(&self) -> RefWrapper<Self>)? What happens with trait forwarding if the trait has such methods?

I'm not sure whether forwarding inherent methods is worth it... it feels like the functionality likely to be forwarded like this should have been a trait instead.

I've been thinking about this too, and it might make more sense not to have the glob forwarding for inherent methods. Just having full trait forwarding:

impl Trait for Struct => self.field

and named method forwarding:

impl Struct {
  fn foo, bar => self.field;
}

might be a better option overall.

3 Likes

I believe that forwarding should be defined in a way that if you change manually written implementation to compiler-forwarded then no change can be observed in produced binary or documentation. Compiler massages only change when referring directly to that piece of code, but should be otherwise same. (e.g. <type of field> doesn't implement Trait)

Globs in traits are reasonable and useful but should not be allowed in inherent impls.

copy+paste/find+replace, which takes only a few seconds to do

If that's so easy then why don't all editors provide this as a shortcut already?

Yes, because it's not actually that easy. You must find the definition, correctly write names and types of all arguments (including paths or use the types), generics... It gets annoying quite quickly.

I use a quickly hacked tool for this in Vim and it only works for hard-coded traits and still makes a bunch of mistakes. Compiler support would be life changing.

They do. Every text editor worth using has copy + paste, and subsequently allows you to edit the pasted text.

You've literally described what I do all day as a software developer. It's called writing code, and it requires that you can read documentation, spell correctly, and understand what you're writing. A feature like this doesn't change that; you will still have to do these things. You'll just be able to do it more quickly if you're doing it in a way that matches this particular pattern, and I don't see it being very common, given that the more prone you are to simply forwarding things along, the less point there is to writing that code in the first place.

Take this example from the OP:

impl ShortSlice {
    fn len(&self) -> usize {
        self.inner.len()
    }
}

This is pretty trivial as written. It's a lot less trivial once you're doing it with an automatic feature that has to resolve whether len is an inherent method or a trait method, or whether it's only available under certain bounds, etc. Or if you go look at this code and see:

impl ShortSlice {
    fn _ => self.inner
}

Which doesn't exactly tell you anything about what this does. And it saves you very little effort when it looks more like this:

impl<T> ShortSlice<T> {
    #[bounds(IsSlice)]
    #[docs(inherit)]
   (pub) fn len => self.inner

You don't save work by using features which require lots of elaboration in order to work correctly. You need the most common bases to be covered, not just the trivial 'do-nothing' cases that are already trivial to write.

A feature like this needs to be better than writing it manually, because otherwise there is no reason to advise using it or to justify its presence in a code base. If your code can be improved by avoiding a feature, then it's not a good feature. Let the editor save you time, and let the language clarify meaning.

2 Likes

I think that if Rust were to implement forwarding it should do it in such a way that the forwarding target can be generated from any expression. There are two motivations for this:

  1. Pin . It can be useful to have forwarding for methods that take Pin<&Self> or Pin<&mut Self> too, but the compiler wouldn't be able to figure out how to pin project from the field name alone.
  2. References. It is tedious to write impl<T: Trait + ?Sized> Trait for &T { ... } and copy out all the implementations, and this is probably best solved with forwarding.

I agree that for something as trivial as:

impl ShortSlice {
    fn len(&self) -> usize {
        self.inner.len()
    }
}

Method forwarding might not be a huge deal. However, when you get to something like:

impl ShortSlice {
    fn len(&self) -> usize {
        self.inner.len()
    }

    fn foo(&self) -> usize {
        self.inner.foo()
    }

    fn bar(&self) -> usize {
        self.inner.bar()
    }

    fn baz(&self) -> usize {
        self.inner.baz()
    }
}

having the shorthand provided by this RFC will really help:

impl<T> ShortSlice<T> {
   #[docs(inline)]
   fn len, foo, bar, baz => self.inner;
}

That said, the main benefit provided by this RFC is trait implementation forwarding.

I have been thinking about this as well. The forwarding described by this RFC could very easily be extended to support arbitrary expressions:

impl Trait for Struct {
    fn foo, bar => { self.get_inner() }
}

Globs in traits are reasonable and useful but should not be allowed in inherent impls.

I think I agree with this as well. As I mentioned above:

It might make more sense not to have the glob forwarding for inherent methods. Just having full trait forwarding:

impl Trait for Struct => self.field

And named method forwarding:

impl Struct {
  fn foo, bar => self.field;
}

Might be a better option overall.

1 Like

The problem with that is what type is self? We could restrict foo and bar to take the same receiver, but in the currently RFC I think they can take any.