Pre-RFC: Forwarding

I would absolutely love to see it implemented! Wapping ffi C structs with this would be a breath

I think this could also be solved with better IDE code actions: IDEs could provide shortcuts for generating these methods automatically. I think this is worth considering.

Is there any reason you chose pattern matching syntax for this beyond just avoiding keywords? I find it really counterintuitive. Also keep in mind that pattern matching syntax combines branches with | not ,.

I think partial and mixed delegation are overly sophisticated and have much less motivation than the simple full delegation case. For those complicated situations I think the person should rethink their trait design or just implement manually when dealing with a foreign trait.

To give an example regarding trait design, I would refactor this

impl Trait for Struct {
    fn foo => self.foo_field;
    fn bar => self.bar_field;
}

into this

// Simpler composable traits
trait Trait: Foo + Bar {}

// Full delegation of a trait to a field
impl Foo for Struct use Self::foo_field {} 
impl Bar for Struct use Self::bar_field {}

impl Trait for Struct {}

By restricting to full delegation, it would encourage better trait design.

I'm not sure what to think about delegation for impl blocks, since it could easily encourage inheritance anti-patterns. It only seems justified for the newtype pattern. One alternative solution is to create a trait and move the methods from the impl block into the trait. This makes sense to me because if methods are defined on multiple types then it sounds like they belong in a trait. Maybe my personal programing style is too trait heavy, though.

It is not really pattern matching syntax, => just seemed like the correct symbol for "forward".

Another advantage of named method forwarding is that you can choose which method you want to re-use when implementing a trait:

impl Trait for Struct => self.field // full delegation
impl Trait2 for Struct {
    fn foo => self.field1; // partially delegated to field1 …
    fn bar => self.field2; // … and to field2 …
    fn baz() { /* implementation */ } // … as well as an add-hock function
}

I'd suggest that in this case it's clearer to just forward them manually without any special syntax... especially given that you likely need to explain this anyway.

If there only one function forwarded, it’s indeed not very useful, but if you want to delegate multiples of them to the same sub-object, it reduce more boilerplate, and makes it explicit what isn’t forwarded.

I think that if we want to add syntax to forward function to inner attributes of a struct, a similar construction should also exist for

  • non-valueless enum
  • where all variants are
    • tuple variants with same arity
    • struct variants sharing a given field name
  • the return type of the forwarded function cannot be impl Trait and must be the same for all variants [1][2]

For enum with tuples variants, the fields can be named with Self::0, Self::1, Self::2, … where the number is the index in the tuple. Likewise for struct variant with Self::foo, Self::bar, … where foo and bar are the name of the attributes in the struct variants.


Given:

enum MyEnum {
    Foo(Foo),
    Bar(Bar),
}
impl MyEnum {
    // the return type of Bar::fct must match the return type of Foo::fct
    fn fct => Self::1.fct;
}

In this fn fct => Self::1.fct would desugar to:

// Assuming both Foo::fct and Bar::fct return the same type Baz
fn foo(&self) -> Baz {
    match self {
        Foo(_1) => _1.fct(),
        Bar(_2) => _2.fct(),
    }
}

A better example would be an enum ForwardOrBackward created from either (for example) my_vec.iter() or my_vec.iter.rev(). Both the Forward and Backward variant implements Iter<Item=T>. This proposal makes it very easy to implement Iter for that struct by forwarding all the methods (or at least next, nth and fold) to the inner variants.

Another one could be an enum, where every variant is a struct with a source field. That source field could be used to implement a private helper. That private helper would itself be used in the implementation of Display of the enum.


[1] To make the return type of the forwarded function impl Trait, we need some kind of compiler-generated anonymous enum (one variant in the anonymous enum per variant of the enum). I think it should be excluded from this initial version.

[2] The return type could be different as long as they all implements Into<Output> in that case, we could declare the forwarding function with a trailing arrow to make the coerced output type. e.g. fn fct -> Qux => Self::1.fct. The desugaring would be the same, but followed by a .into(). e.g. Foo(_1) => _1.fct().into(). I also don’t think it would be needed in the initial version.

1 Like

After a quick look through this thread I didn't catch anyone suggesting the use of Deref for this, which would work as long as inner's type is public:

struct Inner;
impl Inner {
    fn bar(&self) { println!("Inner::bar"); }
}
struct Outer(Inner);
impl Deref for Outer {
    type Target = Inner;
    fn deref(&self) -> &Self::Target { &self.0 }
}
fn main() {
    Outer(Inner).bar();
}
1 Like

Using Deref for something like this is a bad practice, and only works in certain cases.

4 Likes

It has limitations, for sure, but what makes it bad practice?

Deref should be used only when the outer type should behave "as if it were" the inner type completely, not for pseudo-inheritance.

(I.e. it's bad practice if you also have &self methods on the parent.)

Deref originally had a clause saying that it should only be implemented for "smart pointer like" types. That clause no longer exists (iirc), because types like ManuallyDrop<_> and Lazy<_> have also been shown to also have useful Deref implementations that don't cause surprises/problems, as they are supposed to act as a (mostly) transparent wrapper.

See also the Rust unofficial patterns' page on the anti-pattern of "deref polymorphism".

8 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.