New RFC for delegation: anyone interested in contributing?

(small note, override is a reserved keyword, we can use it if we want)

1 Like

override might be useful when delegation meets specialisation. Can someone familiar with specialisation please review this RFC draft?

From a maintainability standpoint, I am not convinced of the utility of * delegations.

  • It should be easy to find where the implementation of a function comes from; “glob” delegations would make that somewhat harder.
  • Additionally, it makes the feature less intuitive: it looks like the equivalent of something like Ruby’s method_missing, where functions not implemented by S are forwarded and “searched for” in f, but of course it’s more like a mixin, where the functions delegated depend on the definition of f. Of course, this “push vs pull” distinction only matters at compile time, but it’s still potentially confusing, I think.
  • This concerns me: "delegate * can automatically change what items get implemented if the underlying trait TR and type F get changed accordingly." That seems like something that the author of S should not want happening automagically based on an update to a separate crate.

For the convenience of the struct author to delegate everything supported by f to f, I think IDEs (or possibly even rustfmt) could support a delegate-everything convenience feature, possibly using delegate * as a shortcut.

I think delegating * is critical for delegating trait implementations, at least. When there’s a new method added to the trait with a default (but often suboptimal) implementation, you want that to be forwarded as well.

6 Likes

What do you think now it's on a new line?

impl TR for S {
    delegate fn foo, fn bar, const MAX, type Item 
        to f;
}

While to field_name; is not required to be on a new line, this will be the rustfmt default when delegating individual trait items, as it makes it easy to visually identify the field being delegated to.

Also, have you seen the Delegate Block way of implementing extensions? I think it'd look a bit weird with two blocks, but you're welcome to add it as an alternative if you feel strongly about it.

I have a couple of thoughts:

  1. I think I prefer self.field_name to field_name as the target. Mostly because it makes the syntax for tuple structs much more obvious:

    delegate * to self.0;
    

    It also avoids ambiguity in situations like:

    static f: i32 = 0;
    impl TR for S {
        delegate * to f;
    }
    

    And I think it is more clear if method delegation is ever supported (self.foo() instead of foo()).

  2. In future extensions, it may be worth mentioning multiple delegation in the same impl. For example:

    impl TR for S {
        delegate fn foo to f1;
        delegate fn bar to f2;
    }
    
5 Likes

Thanks for your thoughts, I agree it makes it clearer for tuple structs. Would you like to add it as an alternative?

My brain mustn't be working well right now, where's the ambiguity in that?

That is already supported without needing an extension. Could you please add an example showing it?

Sure thing.

Thanks @jmst, I've added this to the new RFC draft as the way of implementing the future extension of delegating to methods: Delegate Block.

In a delegation statement, the to keyword and field (delegation target) are replaced with a delegation target block. The block contains a map of parameters to implementer expressions.

e.g. Delegating to an inner type using getter methods instead of fields:

delegate fn foo, fn bar {
    &self => self.get_inner(), 
    &mut self => self.get_inner_mut(), 
    self => self.into_inner(), 
    Self <= Obj::from_inner(self) 
}

NOTE: The last line in the block above is for mapping returned values. However, it doesn't look quite right with self as a parameter of Obj::from_inner, since it's the result of the delegated method that's passed to Obj::from_inner. Should we use a keyword here? Please bikeshed this syntax.

A delegation target block could also be used to implement:

  • Delegating to static values or functions.
  • Delegating to arbitrary expressions.
  • Delegating an inherent impl to a trait impl, or vice versa.
  • Delegating “multiple Self arguments”

While the delegate block is very flexible, I’m not sure about the syntax personally – it’s a bit verbose. It’s also not so clear what it does (what does it do btw?)

I’ve added an exploratory section, Delegating an enum, which will hopefully make it clearer what it does.

I no longer think a delegation target block should be used for common cases, only advanced ones if we decide to support them in the future.

All of the possible future extensions need more design work to tease out the syntax constraints. This will tell us if our syntax is forward compatible with them. We may also find some of them are so complex that it’s worth ruling them out in order to have a simpler, more ergonomic syntax for everything else.

The only way to know is to start writing code examples and see what happens. Please, everyone, pick an extension and start writing some code for it. :slight_smile:

Actually I’m thinking this syntax for the delegate block might be better:

delegate fn foo, fn bar {
    |&self| self.get_inner(),
    |&mut self| self.get_inner_mut(),
    |self| self.into_inner(),
    |x: Rc<Self>| self.rc_into_inner_rc(),
} -> {
    |delegate| Self::from_inner(delegate),
    |x: Rc<Delegate>| Self::rc_from_inner_rc(x)
}

Changes:

  1. Don’t use “<=” since it’s “clever” but is actually not in the transformation order and might cause ambiguities.
  2. Use “->” so it’s similar to function return values, and indicates transformations for returned types (or types in “return position” like return types of closure arguments, etc.).
  3. Use closure syntax instead of “=>” since these are essentially closures
  4. Use “delegate”/“Delegate” instead of “self”/“Self” for the return position, so Self can be used, and it’s less confusing overall
3 Likes

@jmst I understand all the self closure-like stuff, but I don’t get the delegate ones. Are these field names?

Rather than delegate * to F, my instinct would be to end the impl with “…F” like structs. The parallels with structs would reduce the cognitive load.

This sounds neat. Would you care to show with an example?

General comment: A new keyword like delegate might not be needed if impl can be reused in a new context. (not that I have anything against delegate per se.)

I’m thinking of a syntax that utilizes the already well known path/import syntax and that appears outside of the impl block. Personally I see no reason for the delegation to appear inside the impl.

Originally I wanted to avoid the delegate keywoard alltogether and use impl instead. However, this makes the syntax indistinguishable from normal trait impls prefixed with a path (in some cases) and also less readable.

Apologies if something similar to this was already discussed. Thoughts?

// Delegate all
delegate Tr::* for S::f;

// Delegate only hello and world
delegate Tr::{hello, world} for S::f;

// Manual implementation
impl Tr for S {
    // This should error for `delegate Tr::{hello, world} for S;`, but not for `delegate Tr::* for S;`.
    fn hello(&self) {}
}
2 Likes

@Centril, would Delegate need to be a keyword for this? I saw another discussion about contextual keywords complicating the grammar/parser, would it be better to make delegate a full keyword? And what about to?

@Yoric, it's for mapping/transforming the return value of a delegated function.

@Lisoph I remember it being discussed in the original RFC thread and the trend was people preferred it in an impl block, but I don't think it was ruled out. Would you add it to Rationale and alternatives? Personally, I'd be quite happy with doing away with the impl block. :slight_smile:

@gilescope, I too would like to see an example of what you're thinking, it sounds interesting.

@newpavlov, does this solve your use cases? Please modify and "flesh out" the section however you'd like.

@tmccombs, could you take a look at this and add more detail if necessary?

@BatmanAoD, would you like to add your concern to Drawbacks? Personally, it's one of the reasons I love the feature, but perhaps it should be noted when teaching it? If anyone thinks so, please edit the Guide-level explanation.

Thank you, @Ixrec for writing a lot of this. I'd love any further input you might have if you've got time. :slight_smile:

Here are my initial thoughts:

In the way you've used it there (lambda syntax), I think you need a real keyword (tho since it is not necessarily expression context, you could potentially make it contextual, but that is pretty hairy imo.

In the other ways used, (delegate fn .., etc.) it can probably be contextual.

1 Like

Yes, it does, but I think it's an unnecessary overburdening of delegation functionality. Also this approach has several minor issues compared to one outlined in the inherent traits RFC:

  • If all type functionality is expressed purely through traits it requires to add dummy impl block.
  • More typing and reading, compare #[inherent] line or inherent impl with delegate * to trait TraitOne; (plus potential dummy impl block)
  • I am not sure if we need an ability to "delegate" hand picked methods from traits.
  • How this delegation will look in rustdoc? With inherent traits the only change can be just a separate category "Inherent Trait Implementations", instead of duplicating methods.
  • It's a "possible future extension", while in my opinion this feature can have a much bigger impact on ecosystem compared to the delegation (a lot of traits impls can be made inherent to improve ergonomics), so I think it should not be offloaded like that.
1 Like