10

Github RFC: Delegation by elahn · Pull Request #2393 · rust-lang/rfcs · GitHub

 3 years ago
source link: https://github.com/rust-lang/rfcs/pull/2393
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Copy link

elahn commented on Apr 6, 2018

edited

Syntax sugar for efficient code reuse via the composition pattern. Wrapper functions are generated for a struct, delegating most or all of a trait’s impl block to a member field that already implements the trait.

Rendered

Please Note:

This RFC is a group effort from the Rust community. Whenever an issue is raised, please edit the RFC draft to address it as best you can.

If the design needs to be bikeshedded, please do so on this internals thread.

Whenever an issue or question has been resolved, please submit a PR to this RFC.


Thank you, everyone for your contributions, they’ve been a big help. If we continue this collaborative style throughout the RFC process, I’ve no doubt we can address any concerns that arise and get this puppy accepted!

Copy link

Contributor

Centril left a comment •

edited

1001st_place_medal to all collaborators on the heroic effort of writing and collaborating on this RFC!

This RFC seems generally well done; I have some formatting nits here and there and some other "more substantive" (not nits) concerns.

In Rust, we prefer composition over inheritence for code reuse. For common cases, we make this convenient with delegation syntax sugar.

Whenever you have a struct S with a member field `f` of type F and F already implements a trait TR, you can delegate the implementation of TR for S to `f` using the contextual keyword `delegate`:

Centril on Apr 6, 2018

Contributor

Formatting nits: Should probably use backticks on S, F, TR.

In Rust, we prefer composition over inheritence for code reuse. For common cases, we make this convenient with delegation syntax sugar.

Whenever you have a struct S with a member field `f` of type F and F already implements a trait TR, you can delegate the implementation of TR for S to `f` using the contextual keyword `delegate`:

Centril on Apr 6, 2018

Contributor

We probably want to make delegate a real keyword due to current (one day old) lang team keyword policy that new features should be real keywords for maintenance reasons.

Here is a quick review of the breakage risk:

  • TL;DR: The risk is quite minimal and something we could probably live with.

  • Usage as ident in libstd: No

  • Usage as the name of a crate: No

  • Usage as idents in crates (sourcegraph): 19+ uses

}

```

This is pure sugar, and does exactly the same thing as if you “manually delegated” all the items of TR like this:

Centril on Apr 6, 2018

Contributor

Formatting nit: backticks on TR

```

Aside from the implementation of foo(), this has exactly the same meaning as the first example.

Centril on Apr 6, 2018

Contributor

Formatting nit: backticks on foo()

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.

Centril on Apr 6, 2018

Contributor

A good default here could be that fn is assumed (since it is most common), and so you could instead write:

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

another possible shorthand notation could be (but I am not proposing it at this point):

impl TR for S {
    delegate
        fn { foo, bar, the_hulk, black_widdow },
        const { MAX, MIX },
        type { Item, Baz, Bar }
        to f;
}

but you are allowed to prefix with fn if you so wish.

We expect to resolve through the RFC process before this gets merged:

- Is it useful and/or feasible to allow delegation statements to appear anywhere in the impl block, rather than all at the top?

Centril on Apr 6, 2018

Contributor

My gut feeling is that it is feasible. delegate <stuff> to <field>; should be considered an item, and as such, you'd [rough sketch] first do a pass collecting all the items and after that you simply extract the delegate items and desugar those into a flat set, remove all the idents (LHS) found in the remaining set from the flat set, and then you finally merge the sets.

I think rustfmt should put them at the top, but people should be able to do as they like and the grammar should be flexible enough to accommodate that if it is technically possible.

- 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?

Centril on Apr 6, 2018

Contributor

I think the initial set is conservative enough +1

- 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?

- Although the syntax and desugaring for "delegating some methods to one field and some to another" is straightforward, should it be postponed as a possible future extension?

- Are there any cases of [Custom Self Types](https://github.com/rust-lang/rfcs/pull/2362) where self needs to be manually dereferenced, e.g.

Centril on Apr 6, 2018

Contributor

Formatting nit: backticks on self.

If so, can these be handled during implementation of this feature or is upfront design work required?

- There is a concern about _inherent traits_ causing duplicated symbols, can this be resolved during implementation?

- Is the possible future extension _delegate block_ ruled out? If not, keywords `delegate`/`Delegate` should be reserved in edition 2018.

- Should we implement the proposed syntax or one of the alternatives in nightly? We may wish to gain experience using a particlular syntax on nightly before committing to it.

Centril on Apr 6, 2018

Contributor

That could be done concurrently with the RFC if someone has the time, but I don't think an experimental RFC is necessary here, the current design is pretty good.

We expect to resolve through the implementation of this feature before stabilization:

- How does delegation interact with specialization? There will be a [default impl](https://github.com/rust-lang/rfcs/blob/master/text/1210-impl-specialization.md#default-impls) block in the future. Should we allow `delegate` to be used in a `default impl` block?

Centril on Apr 6, 2018

Contributor

What would the reason to not allow them be?

Copy link

Author

elahn commented on Apr 6, 2018

In the previous revision:

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.

Unresolved question: Is it useful and/or feasible to allow delegation statements to appear anywhere in the impl block, rather than all at the top?

@Centril: My gut feeling is that it is feasible. delegate to ; should be considered an item, and as such, you'd [rough sketch] first do a pass collecting all the items and after that you simply extract the delegate items and desugar those into a flat set, remove all the idents (LHS) found in the remaining set from the flat set, and then you finally merge the sets.

I think rustfmt should put them at the top, but people should be able to do as they like and the grammar should be flexible enough to accommodate that if it is technically possible.

@Ixrec: Based on my memory of past and current delegation threads:

  1. Some people thought this was required to make the contextual keyword option viable. I think this line of reasoning predates epochs/editions. It's obviously obsolete now that we have official guidance on new keyword introduction.
  2. When an impl block contains a delegation as well as a regular item implementation, it seems better style to put the delegation on top. Especially when it's a * delegation, since "Foo implements trait T by delegating almost everything to field f, except that method foo is ... " is the natural way to describe that sort of thing.

So imo, as part of editing this to say delegate should be a real keyword, we should also delete this restriction. Putting the delegate item(s) at the top is probably still good style, but there's no need to make it mandatory.

Unresolved question: 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?

@Centril: I think the initial set is conservative enough +1

These questions are considered resolved unless someone objects.

impl AddAssign for AddOnlyCounter {

delegate * to 0;

}

scottmcm on Apr 7, 2018

edited

Member

I was already thinking of suggesting that it be delegate <list> to self.<field>, and this solidified the feeling. (delegate fn hash to 0 looks like it'd do 0.hash() to me.) I like there being restrictions as a starting point, but I wouldn't be surprised at all for this to get expanded, and expanding to expressions (perhaps a subset thereof, but note that unimplemented!() would just work if it took anything) seems like the most obvious one, so I think the syntax should at least look like a normal expression from the beginning. (Readers: feel free to +1(-1) if you would(n't) like me to make a PR for that.)

Edit: Oh, I see the mention of this in the alternatives. I'm not convinced learnability is a big problem; even with just a field name it seems like it's "oh, they'll put self. in front" and expect more things to work, and it'd be easy to give a very clear error for "delegation only supports a single field". More abstractly, is there a reason it couldn't support an arbitrary expression? There's already a "implementer expression" concept in the reference section below. (Well, ones where let _ = expr; could infer the type, giving the same "cannot infer type for T" errors you get from things like that let if you tried to do something like delegate * to self.0.into();.)

Centril on Apr 7, 2018

Contributor

Personally, I'm quite torn on the subject of self vs. not. While self.field is clearer, it is also a bit more verbose; and you could support arbitrary expressions with a { } enclosing of { unimplemented!() }.
With respect to error messages and learnability I agree that this isn't a big concern.

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.

There may be more than one delegation statement. For readability, `rustfmt` moves delegation statements to the top of an impl block.

scottmcm on Apr 7, 2018

edited

Member

Perhaps justify why this is more readable? I can see an argument for *, but especially for single-method delegation, I'm not convinced. Particularly in a case where I'm changing existing code to replace manual delegation with delegate, I'd expect rustfmt to leave the the item where it is so the diff is the obvious one. And even in new code, I might be intentionally matching the order of the methods on the trait, for example. (Nit-picky: if it's going to move multiple, it would need to pick an order to put them in.)

Centril on Apr 7, 2018

edited

Contributor

My current thinking here is that:

  • The argument starts from *, which should be at the top so that it is seen first
  • For consistency with *, you place all delegation where delegate * to whatever would be

An argument could however be made that all delegations should be at the bottom since they often will be delegate * and so they are a sort of "catch the rest", i.e, they function like match x { important => .. , _ => .. } does wrt. _ =>.

With respect to order, I'd first group items by item type and then in each group alphabetically so:

  • const

scottmcm on Apr 8, 2018

Member

BTW, does rustfmt reorder any other items? I tried on play and it doesn't seem to reorder type and fn in an impl block, for example...

Centril on Apr 8, 2018

Contributor

Interesting!

PS: we could leave formatting bikeshed up to a style-fmt RFC.

Copy link

Member

scottmcm commented on Apr 7, 2018

What happens when things go wrong? For example, what happens if I try to

impl Default for AddOnlyCounter {
    delegate * to 0;
}

Is there a way to map output (and output types)? For example, if I do

impl Add for AddOnlyCounter {
    delegate * to 0;
}

It seems like the output type is most likely u32, but I'd probably want it to be AddOnlyCounter. And what's the RHS? Does it just fail because the impl wants the RHS to be AddOnlyCounter, but self.0.add wants a u32?

In general, it seems like Self anywhere that's not also self is a landmine here.

```rust

fn check_name(&self, name: &str, ignore_capitals: bool, state: &mut State) -> bool {

self.f.check_name(&name, ignore_capitals, &mut state)

}

scottmcm on Apr 7, 2018

Member

I don't think I know what "according to their type" means here. Why are the extra &s needed, instead of just moving them all? Perhaps self.f.check_name({name}, {ignore_capitals}, {state}).

, MonadWriter Unique )

```

This is massive code reuse and not in any OOP language ^,-

Centril on Apr 7, 2018

edited

Contributor

o.O you copied in this verbatim =D

Copy link

Contributor

petrochenkov commented on Apr 7, 2018

Yes, please (ignoring specific syntax).
This has potential to remove more useless boilerplate than all the "ergonomic initiative" RFCs combined.

impl TR for S {

delegate * to field_one;

delegate fn foo, const MAX, type Item

to field_two;

tanriol on Apr 7, 2018

Would be nice to have a bit more motivation for this. Treating a trait as a unit of behavior, in what situations does it make sense to delegate some behavior to one field and some to a different one?

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 authors' rationale for preferring `delegate ... to field_name;` over all of these alternatives.

- `impl TR for S use self.F { ... }` was criticized in the first RFC’s comment thread for looking too much like inheritance.

jan-hudec on Apr 7, 2018

Why is that a problem? It basically is inheritance. Or a restricted version of it, because inheritance delegates all methods to the designated member called “base” while this only delegates methods of specific trait. But that restriction is obvious from this syntax.

Ixrec on Apr 7, 2018

edited

Contributor

"Inheritance" traditionally means a LOT more than simply delegating implementations to another type. For instance: committing to the same interface, having the same internal structure as another type, or being implicitly convertable to other types. In Rust those things are usually kept separate, which we'd like to keep doing.

So if we wanted to make this part of the RFC clearer, we could say that impl TR for S use self.F {} makes it surprising that a field named F must already exist in S's declaration, rather than being implicitly added by this impl TR for S use self.F {}. Or we could say that impl TR for S use self.F {} makes it surprising that S cannot be implicitly converted to an F. And so on.

I don't personally think there's any one or two specific features in the "inheritance" grabbag that most users would mistakently expect when they see impl TR for S use self.F {}, but I do think a lot of people would mistakenly expect something on top of method delegation if they saw that syntax. It might be that "it looks too much like inheritance" is the only concise way to present that argument without getting into the weeds of what people think "inheritance" means.

jan-hudec on Apr 8, 2018

Inheritance does not mean all that much more than delegation, really. It does mean that all interfaces will be delegated, but it should be obvious it's not happening here, because it starts by stating which trait it is delegating. It does not mean committing to the same structure, only to containing that structure, but that needs to happen here as well. The only other thing inheritance does is the implicit coercion of reference. That won't happen here, but we only said we are delegating specific trait after all. And coercion to that trait does work.

This form would not be appropriate for delegating inherent methods, because that would look like it might be doing more than it does. But for the traits, it would be convenient shortcut that does not really look like promising more than it does.

makes it surprising that a field named F must already exist in S's declaration

Since it does not mention the type of F, it's quite clear that that still has to be given.

makes it surprising that S cannot be implicitly converted to an F

I would somewhat agree for inherent methods, i.e. impl S use self.F {}. But I wouldn't fear that much for the impl TR for S case.

jan-hudec on Apr 8, 2018

Hm, but there is one difference that I have to admit would be quite confusing—in case of inheritance, the overridden behaviour applies even if you've got a reference to the “base field”. But in our case it won't. In fact, I fear delegating some methods, but not all of them, will be confusing with any syntax.

Copy link

Contributor

Diggsey commented on Apr 8, 2018

It's possible I missed it, but there's no mention of delegating to nested fields, eg. delegate * to x.y - it seems... odd to constrain it to a direct field.

Copy link

Contributor

WiSaGaN commented on Apr 8, 2018

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.

I still prefer this syntax proposed by @eddyb . This syntax does not introduce new keyword (contextual or not). And the meaning is obvious, and "feels" just right. I would argue that we should not encourage "glob" delegations. I am wondering how many cases are left for "glob" if we can use impl MyTrait for MyType => self.0; to delegate all methods in a trait.

```rust

impl TR for S {

fn foo(self: Box<Self>) -> u32 {

self.f.foo()

Nemo157 on Apr 8, 2018

edited

Member

I don’t believe the linked Custom Self Types RFC adds support for implicit projections of smart pointers (I don’t think it is possible in general, given an Rc<T> you can’t obtain an Rc<SomeFieldOfT>), which would be necessary for this to work.

Copy link

Contributor

Ixrec commented on Apr 8, 2018

@WiSaGaN

I would argue that we should not encourage "glob" delegations.

Do you mean "glob delegation" as in any syntax to delegate everything not explicitly implemented, or just the use of * in the proposed syntax? I would've thought "glob delegation" meant the former, but you appear to be proposing that impl MyTrait for MyType => self.0; do exactly the same thing in the very next sentence.

Copy link

Contributor

WiSaGaN commented on Apr 8, 2018

edited

@Ixrec the RFC states the shortcoming of fn method = self.field.method; syntax is the inability to handle multiple, most or all items in an impl. I assume this means the cases not covered by impl MyTrait for MyType => self.0;. In these cases, explicitly spelling out the functions one need instead of conveniently pulling in all functions not grouped by any traits seems desirable because it

  • promotes conservatively exposing functions in the new type that reduces the possibility of accidentlly pulling in unintended functions
  • minimizes reader/maintainer's search effort for what are delegated exactly

I'm happy that there are people working on this as I find the motivation compelling. But one of the reasons #1406 was closed was that it tried to do too much, with too much syntax. I think this RFC still has that same problem. Personally the only feature I want is to delegate an entire trait implementation to a field. An attribute would do fine, I see no need for first-class syntax.

Copy link

Contributor

Ixrec commented on Apr 8, 2018

edited

@leodasvacas To clarify, are you only talking about the introduction of dedicated syntax, or do you also feel this RFC "does too much"? It sounds like you're saying both, but the actual functionality proposed here (i.e., ignoring all of the possible future extensions) is a small subset of what #1406 proposed and under-specified. And part of the reason #1406 was closed is because its proposed reuse of an existing keyword was potentially confusing and ambiguous; now that we have epochs/editions introducing a new keyword is intended to be an improvement on it (if you have a specific alternative to a new keyword without the problems of #1406 I'd love to hear it).

@WiSaGaN I honestly can't figure out what you're trying to say in that comment. Are you objecting to the feature of delegating all items? Or the feature of delegating most/all non-explicitly implemented items? Or some specific syntax for either or both of those? I believe impl MyTrait for MyType => self.0; syntax can handle the all case but not the most case, while I don't see how fn method = self.field.method; could handle either.

Copy link

Contributor

WiSaGaN commented on Apr 9, 2018

@Ixrec We may be able to use impl MyTrait for MyType => self.0; with fn method = self.field.method;. These two won't cover the case where a type has many inherent methods, and you want to delegate all of them in one line, and the "most" cases. By using fn method = self.field.method; in these cases, you typed more, but it has the two benefits listed in my previous comment.
This approach though, does not introduce "delegate", nor it introduces the "*".

Copy link

Contributor

clarfonthey commented on Apr 10, 2018

edited

One other argument in favour of using self.field instead of field is this:

impl<'a, T: 'a + Trait> Trait for &'a T {
    delegate * to *self;
}

Which, IMHO, should be allowed from the start. So many traits do this and I think it's extremely reasonable to include it. Additionally, disallowing delegate * to self at least when deny(unconditional_recursion) is on is also super important; personally, I'd even say that this should be a hard error.

I think that it should be clarified that this syntax should not alter the behaviour compared to the manual equivalent. To the point where, if they differ, it should be considered a compiler bug. This goes either way; if something works with delegation (as it should) when it fails manually, I'd consider it a bug.

Copy link

Contributor

kornelski commented on Apr 10, 2018

edited

Could I delegate Ord to a combination of two fields?

struct MyStruct {
   field_a_ignore_me: Foo,
   field_b: u8,
   field_c: String,
}

impl Ord for MyStruct {
    delegate * to |&self| (self.field_b, self.field_c);
}

Copy link

Contributor

Ixrec commented on Apr 10, 2018

@kornelski No. That probably falls under this item on the list of possible future extensions:

Delegating “multiple Self arguments” for traits like PartialOrd, so that delegate * to f; would desugar to something like self.f.partial_cmp(other.f)

Copy link

Contributor

Havvy commented on Apr 11, 2018

Nit: This is proposing a delegate associated item, not a statement.

Copy link

Author

elahn commented on Apr 12, 2018

@scottmcm: What happens when things go wrong? For example, what happens if I try to

impl Default for AddOnlyCounter {
    delegate * to 0;
}

A nice compiler error explaining Default isn't implemented for u32, so the implementation for items of Default cannot be delegated to AddOnlyCounter::0.

Is there a way to map output (and output types)? For example, if I do

impl Add for AddOnlyCounter {
    delegate * to 0;
}

It seems like the output type is most likely u32, but I'd probably want it to be AddOnlyCounter. And what's the RHS? Does it just fail because the impl wants the RHS to be AddOnlyCounter, but self.0.add wants a u32?

Since we're delegating to u32, the signature of u32.add() is used, so RHS is u32:

impl Add for AddOnlyCounter {
    type Output = <u32 as Add>::Output;

    fn add(self, other: u32) -> u32 { self.0.add(other) }
}

To map the output type using the possible future extension "delegate block":

impl Add for AddOnlyCounter {
    type Output = AddOnlyCounter;
    delegate * {
        |self| self.0
    } -> {
        |delegate| AddOnlyCounter { delegate }
    }
}

// Generated code:
impl Add for AddOnlyCounter {
    type Output = AddOnlyCounter;

    fn add(self, other: u32) -> AddOnlyCounter {
        AddOnlyCounter { self.0.add(other) }
    }
}

For RHS to be AddOnlyCounter, the possible future extension "delegating multiple Self arguments" would be needed.

Copy link

mehcode commented on Apr 16, 2018

edited

I think I'm missing the point here. Couldn't this be trivially provided by a crate and a proc macro for the common case? Does it really need to be syntax?

#[delegates(Trait, "bar")]
struct Foo { bar: Bar }

Copy link

Contributor

clarfonthey commented on Apr 17, 2018

@mehcode the proc macro would have to have the ability to tell what methods Trait has, so, it'd be extremely limited.

Copy link

Member

scottmcm commented on Apr 19, 2018

Meta-comment: I definitely want some form of delegation, but I think this RFC could still do a better job of carving out a simple and uncontroversial subset. For example, when I think of delegating Add for struct MyNewtype(u32), I expect MyNewtype: Add<MyNewtype, Output=MyNewtype>. This RFC doesn't do that. And that's fine; I'd just rather, then, that it was a "isn't supported for delegation at this time" error. Maybe a simple rule can be found for "delegation-safe" items (like there's a rule for "dyn-compatible traits) that this can be restricted to for now, with additional support added later for the tricky cases.

First Draft: Only methods that don't mention Self other than in self, &self, and &mut self.

A nice compiler error explaining Default isn't implemented for u32

Umm, u32 is Default: https://doc.rust-lang.org/beta/std/primitive.u32.html#impl-Default

@hobofan which exact concerns are there? I don't remember anything important, since it's just plain stupid forwarding.

I disagree that this can be done using macros. Macros can't see definitions of traits from other crates. I actually proposed to just expand macros but quickly found there's a significant problem: impossible to determine the order of macro expansion. This may be one of the reasons why ambassador is not popular - the interesting traits are missing. (I guess the strange name is another reason.)

That being said, I didn't have need for this for a wile except for impl fmt::Display for struct MyNewtype(String);, so you still may have point. I guess one of the reasons behind wanting it was wanting to silence people whining about lack of inheritance. So an alternative way to silence them is saying "Delegation is better and manual delegation is rare." I still might be completely wrong and people may manually delegate more than I do.

Copy link

ibraheemdev commented on Nov 25, 2020

edited

Well, take a look at the actix_web::HttpRequest:

pub struct HttpRequest(pub(crate) Rc<HttpRequestInner>);

impl HttpRequest {
    pub fn head(&self) -> &RequestHead {
        &self.0.head
    }

    pub fn match_pattern(&self) -> Option<String> {
        self.0.rmap.match_pattern(self.path())
    }

    pub fn match_name(&self) -> Option<&str> {
        self.0.rmap.match_name(self.path())
    }

    pub fn extensions(&self) -> Ref<'_, Extensions> {
        self.0.head.extensions()
    }
    // ...
}

This boilerplate could be removed with delegation, or struct embedding (#2431). I think some form of efficient-code-reuse is needed in a language without inheritance. Go handles this with embedding. Because Rust does not allow implementing external traits for external structs and instead encourages the newtype pattern, delegation/embedding makes sense in the language.

How do other general-use non OO languages (haskell, etc.) handle this?

Copy link

hobofan commented on Nov 25, 2020

edited

@Kixunil What are you goals here apart from discounting all the work that's been put into the RFC, by calling everything in it "bikeshedding", and the whole feature "plain stupid forwarding" (which it isn't)?

Merging RFC doesn't say anything about stabilization.

Merging a RFC normally indicates that a feature has generally been accepted for future implementation and all the major pitfalls have been ironed out (or will likely be ironed out along the way). Postponing that important work to a later stage, where there is usually less community involvement can lead to not-so-nice situations where the community feels like they've been locked out from important discussion (as has happened in the case of deciding on the .await syntax post-RFC). Just accepting an RFC without any intent to implement and stabilize it (especially if it might have design flaws) is a bad idea.

In the case of this RFC there doesn't seem to be a consensus on the syntax of the proposal. There also doesn't seem to be a consensus that the feature is important enough to make it into the language at all (if #2429 is any indication rather not). As @ibraheemdev correctly points out #2431 (which itself isn't even a proper RFC yet) might also be a viable alternative that might be preferable, but neither issues have received a lot of discussion since that one was opened.


I disagree that this can be done using macros.

While I don't particularly care about ambassador these days, I think one of the main points it proved is that it can be done with new mechanisms that have become available to the macro system since earlier discussion.

Macros can't see definitions of traits from other crates.

Ambassador has a mechanism specifically for that, and even for the specific fmt::Display case. As you correctly point out the "interesting traits" are missing, as I didn't bother to go through all the standard library traits and including them in some built-in way. IIRC there is nothing that prevents that from being done.

If you want to go really crazy you could possible forgo that whole mechanism and get to the trait information via other ways (which I don't really want to expand on, as I might be burned at the stake for doing so).

Ambassador has a mechanism specifically for that

Yes, but it isn't really an elegant mechanism. It requires copy-pasting code from another crate, and keeping it up to date.

I do think it would be good to have this done in macros, but a really nice solution would require macros to have more type information. Personally I would rather see movement towards having some sort of type-aware macro system than this specific use case.

Copy link

burdges commented on Nov 26, 2020

edited

I think struct embedding #2431 never became an RFC because it invites too many complications (semver, diamond pattern, enums, etc.) and never produced many useful examples.

Ambassador looks sufficient for delegation honestly. I suppose nobody asked for syntax like #[delegate(Trait, invoke = "method")] yet, which kinda answer a discussion above. I'm surprised #[delegatable_trait] does not require any options, very nice.

I think #[delegatable_trait_remote] looks fine now, but proc macros could implement a #[delegatable_trait_fetched(Trait)] that actually fetches and replicates the definition. But why bother?

I'd expect partial delegation like discussed above to be more useful than remote delgation, although if few people use delegation anyways.

#[delegatable_trait]
trait Foo {
    .. many methods ..
}

struct Bar { }
impl Foo for Bar { .. }

#[derive(Delegate)]
struct Baz { baz: Baz, .. }

#[delegate_partial]
impl Foo for Baz {
    .. just one method ..
}

It'd rock if rust provided some explicit macro cache, so you could read macro outputs under target/.

I think #[delegatable_trait_remote] looks fine now

Consider the case where you have a crate where you have a single struct (or enum) that you want to implement a large trait (like, say slog::Serializer ), that delegates every method to one of its fields. ambassador doesn't really buy you anything there, because you still have to provide the definition of the trait.

There are certainly cases where ambassador is useful, but I suspect it is fairly common where a crate only needs to delegate a trait for a single data type within that crate. And in that case ambassador doesn't not provide a significant benefit.

@hobofan hard to tell over text but it seems to me you had a bad day or something. That's fine, I'm not offended/affected. Just want to let you know I definitely didn't want to discount any work and I didn't call the RFC bikeshedding, but the discussion that proposes tons of various syntaxes. My idea is to get something working in nightly to see how it behaves in real life as that might actually help indecision if people are allowed to try it out.

Thanks for pointing out the issue with await. Is there a way we can fine-tune it? Perhaps giving people louder heads-up?

Could you explain what exactly is not simple about such forwarding? My thinking is if we could write a hypothetical macro that is guaranteed to be non-recursive, expanded last and can take definition of any item just by specifying the path of this item, then it could be done with such a macro.

I didn't bother to go through all the standard library traits and including them in some built-in way

And this is exactly the problem I'm talking about. Even you, the creator of crate supposed to address the problem, were not motivated to do the work. Such work is tedious and uninteresting and could be done by a machine - what this RFC is all about.

Copy link

hobofan commented on Nov 26, 2020

edited

@Kixunil

My idea is to get something working in nightly to see how it behaves in real life as that might actually help indecision if people are allowed to try it out.

Thanks for pointing out the issue with await. Is there a way we can fine-tune it?

The best way to avoid similar situations is to try out as much as possible in the compiler already. If someone wants to implement a prototype of this feature directly in the compiler for people here to try out and evaluate against different approaches, they are already free to do so right now, and that has been done for RFCs with similar syntactic concerns in the past. There is no need for an RFC to be accepted for someone to start doing so and on the flip side an implementation won't just start materializing in nightly by accepting an RFC unless there is someone motivated to implement the feature anyways.

Could you explain what exactly is not simple about such forwarding?

Of course the underlying mechanism is "just" forwarding, but I think the RFC already lays out all the non-simple parts about it (e.g. partial forwarding). That paired with arriving at a good syntax makes this feature non-trivial in my opinion. If one is already is set on the syntax as laid out in the RFC, it might indeed be straightforward to implement (I don't know enough about the compiler internals to judge that).

Even you, the creator of crate supposed to address the problem, were not motivated to do the work.

I'm in no way "supposed" to do that work. I wrote and published the crate mostly to show that previous limitations of the macro system can finally be worked around and to serve my use-case at the time that wasn't handled well by existing crates (delegating large traits through multiple levels). If there is a pressing problem to a user of the crate they are free to contribute that or even just open an issue for that. (In particular for your example of delegating std:: traits on newtype structs, I've always been satisfied with derive_more).

There is no need for an RFC to be accepted for someone to start doing so

Oh, I had the impression that RFC is required. Thanks for clarifying!

This comment has been minimized.

Copy link

Kixunil commented on Jan 14

@hhggit could you also provide an example of case when there is an inherent fn and trait fn with the same name and when there are two or more trait fns with the same name?

I started a thread proposing a variation of "delegation" called "forwarding" (Pre-RFC: Forwarding) which is closer to the syntax suggested by @eddyb and @WiSaGaN. This is a very highly requested feature and that consensus seems to be that it makes sense for it to be included in the language. Hopefully we can decide on a path forward soon!

Copy link

ibraheemdev commented 25 days ago

edited

I've been thinking about this issue more lately, and I've come to the decision that the design outlined by this RFC is probably the best way to go about it. It outlines a much simpler and straightforward solution to the problem than any other proposals I have seen (#493, #2431), although some of those proposals cover more use cases and should not be discounted. This issue comes up a lot from beginners. You explain to them that Rust favors composition over inheritance, and then they get confused as to why there is so much boilerplate. Someone wants to implement an external trait for an external type, and you tell them to new-type it, but then they lose all the other trait implementations and have to resort to copy pasting self.0.foo() everywhere, which again, is very confusing. For a language that argues against inheritance, it feels very odd that there is no solution to efficient code re-use. External crates like delegate cover some use cases, but delegating an external trait implementation is still only possible manually, or with hacks like ambassador's copy-paste solution, and again, it is confusing that Rust favors composition over inheritance, but you often still need an external library (both of which depend on syn - compile times) to get rid of boilerplate. Many jump to implementing Deref, which was not it's original purpose, and yet is recommended by the Rust book. Can we move forward with this RFC or get some triage from the lang team? I feel that this is a very important issue, but the discussion around it has died down. Rust 2021 edition is coming up soon, and this RFC introduces a new keyword (delegate), so it seems like a good time to get things started up again.

Copy link

Kixunil commented 24 days ago

Wanted to help this move so I gave it another read. I agree that this proposal is very good. I'm just not sure about * delegating everything including types, while explicit syntax doesn't allow delegating types. I'm not convinced forbidding types at first is beneficial. I suspect the difficulty of implementing it is marginal.

Copy link

Contributor

nikomatsakis commented 12 days ago

@rfcbot fcp postpone

Hello everyone; we discussed this RFC in our backlog bonanza. The consensus was that we that we should postpone it, as we don't think we have the bandwidth to see to it right now, but we are very interested in this general direction, and we agree that this addresses some real problems in Rust.

We would like to encourage folks to discuss it when the time comes for us to discuss our upcoming roadmap (one of the procedural changes we have in mind is to make it clearer when we'd be open to bigger proposals).

Speaking for the team, I do want to apologize. I realize that we encouraged @elahn and others to do this design work, and then let it languish and I'm sorry about that. :(

Copy link

rfcbot commented 12 days ago

edited by nikomatsakis

Team member @nikomatsakis has proposed to postpone this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

Copy link

rfcbot commented 12 days ago

bellThis is now entering its final comment period, as per the review above. bell

Copy link

Contributor

nikomatsakis commented 12 days ago

I've checked the names of those folks who were present in the meeting.

Copy link

rfcbot commented 2 days ago

The final comment period, with a disposition to postpone, as per the review above, is now complete.

As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.

The RFC is now postponed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK