Skip to content

Function delegation - #3530

Open
petrochenkov wants to merge 17 commits into
rust-lang:mainfrom
petrochenkov:delegation
Open

petrochenkov wants to merge 17 commits into
rust-lang:mainfrom
petrochenkov:delegation

Conversation

@petrochenkov

@petrochenkov petrochenkov commented Nov 15, 2023 •

Copy link
Copy Markdown
Contributor

View all comments

Summary

This RFC proposes a syntactic sugar for delegating implementations of functions to other already implemented functions.

There were two major delegation RFCs in the past, the first RFC in 2015 (#1406) and the second one in 2018 (#2393).
The second RFC was postponed by the language team in 2021 (#2393 (comment)).
We hope to revive that work again.

This proposal is based on the experimental implementation of the feature in nightly rustc tracked in rust-lang/rust#118212.

Rendered

petrochenkov added a commit to petrochenkov/rust that referenced this pull request Nov 16, 2023
In accordance with the [process](https://github.com/rust-lang/lang-team/blob/master/src/how_to/experiment.md).

Detailed description of the feature can be found in the RFC repo - rust-lang/rfcs#3530.

TODO: find a lang team lang-team liaison.
@joshtriplett joshtriplett changed the title eRFC: Implement function delegation in rustc Implement function delegation in rustc Nov 22, 2023
@traviscross traviscross added the T-lang Relevant to the language team, which will review and decide on the RFC. label Nov 22, 2023
@traviscross

Copy link
Copy Markdown
Contributor

@petrochenkov: In the T-lang meeting, some people who had read the RFC were feeling good about it, and there was some interest in proposing FCP merge on this as a normal RFC in addition to going forward with the experiment while the FCP is pending.

However, some text in the body of the RFC and in the PR description here describes this RFC as an experimental one, which, as you note, is no longer a thing.

If you're ready for this RFC to move forward as a normal RFC, if you could, please remove the language about the RFC being experimental.

@Kobzol

Kobzol commented Nov 23, 2023 •

Copy link
Copy Markdown
Member

My 2 cents (as a maintainer of https://crates.io/crates/delegate, which is probably the most used crate for delegation, in combination with https://crates.io/crates/ambassador):

There is a lot of various configuration options/knobs and variants of delegation that are important to different users, which can be seen from the myriad of options implemented in the delegate crate over the years (and there are still some use-cases in our issue tracker that haven't been even implemented yet).

Here is an example of a few such things:

  • Should the forwarding functions be inline or not? Should they be marked with some other custom attributes?
  • Should their return values or input parameters be coerced somehow? Into, TryInto, AsRef etc.? Should the result be unwrapped?
  • Do I want to change the delegated function name?
  • Should additional expressions be passed to the forwarded methods?
  • Should await be called on the forwarded methods?

It's probably not practical to add support for all these use-cases on the language level. A language level solution would hopefully solve the most common use-cases, and ideally also provide some extension points to go beyond these basic use-cases, but that might be quite challenging. If the language won't solve the problems of Rust users, they will just go back to using a third-party crate. I think that for gauging which problems are actually the most important, it might be better to take a look at the crates that use delegate or ambassador, and examine their usage patterns, rather than scanning the source code and trying to derive possible delegation patterns from that (although having this code-mining platform available will be definitely incredibly useful too!).

From my view, the most important aspect of delegation (that is not easily possible to perform with third-party crates at the moment) is the automatic enumeration of things that should be delegated (e.g. the signatures of methods of a trait), rather than the "forwarding" itself. It's quite common to delegate a trait impl to a field of a structure (or to a field of each enum variant), but since we don't have the ability to query the existing signatures, we have to both enumerate the method names, and also repeat their signatures.

Thus for me, a solution to delegation in Rust could be to implement a different language feature - some form of reflection that would give us the ability to code-gen the signatures of methods based on their names, and also the signatures of all methods of a trait. If this was available, any third-party crate (such as delegate) would be able to implement pretty much any delegation pattern without any further support from the language. Right now, delegate doesn't know the signatures of trait methods, so the user has to repeat the signatures, which is quite annoying. Repeating only the names of the functions would be already much better. With some form of reflection being available, we could provide the low-level building block in the language (and reflection is also useful for many other things), but leave the many details to third-party crates that are much easier to iterate upon than the language itself.

That being said, if we decide to go with the "delegation in language" way, I think that it should support the automatic enumeration of trait methods, and perhaps as an MVP also just enable delegating traits with some simple syntax. I like how it works in Kotlin, where you specify the interface (so a trait) that you want to delegate, and the expression/field that you want to forward that interface to. From my experience at least, this is the most common use-case for delegation.

So e.g. it would be great if something like this was possible to do with "language-level delegation":

struct Wrapper(Foo);

impl Bar for Wrapper by self.0 {
  // optionally allow to override the generated forwarding implementations
}

petrochenkov added a commit to petrochenkov/rust that referenced this pull request Nov 23, 2023
In accordance with the [process](https://github.com/rust-lang/lang-team/blob/master/src/how_to/experiment.md).

Detailed description of the feature can be found in the RFC repo - rust-lang/rfcs#3530.
@ericsampson

Copy link
Copy Markdown

I think that for gauging which problems are actually the most important, it might be better to take a look at the crates that use delegate or ambassador, and examine their usage patterns, rather than scanning the source code and trying to derive possible delegation patterns from that.

I agree, that sounds like an additional useful source of data!

@petrochenkov, do you have any interest in the following approach? I suppose they’re not mutually exclusive—there could be language support for the common delegation scenarios, and eventually also add reflection support to enable crates to “fill in the rest” of the more complex/niche functionality. Plus, better reflection support would unlock a lot of other functionality for Rust.

Thus for me, a solution to delegation in Rust could be to implement a different language feature - some form of reflection that would give us the ability to code-gen the signatures of methods based on their names, and also the signatures of all methods of a trait

bors added a commit to rust-lang-ci/rust that referenced this pull request Nov 23, 2023
…shtriplett

Add an experimental feature gate for function delegation

In accordance with the [process](https://github.com/rust-lang/lang-team/blob/master/src/how_to/experiment.md).

Detailed description of the feature can be found in the RFC repo - rust-lang/rfcs#3530.

TODO: find a lang team liaison - https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/fn.20delegation.20liaison/near/402506959.
@petrochenkov

petrochenkov commented Nov 23, 2023 •

Copy link
Copy Markdown
Contributor Author

@traviscross
I've removed the process disclaimer.
It's probably fine to treat this as a normal compiler & language RFC for an experimental feature, the text is relatively detailed.

But it will certainly need a second iteration with finalized syntax and other design choices before any attempts at stabilization.
I also didn't add some clauses typical for proper language RFCs, like guide-level explanation, high-level alternatives (e.g. reflection as mentioned by other commenters) or drawbacks.

@petrochenkov

petrochenkov commented Nov 23, 2023 •

Copy link
Copy Markdown
Contributor Author

@Kobzol @ericsampson
I did look at delegate, ambassador and everything else in literature before collecting my statistics, that knowledge sort of guided which statistics to collect.

Regarding the specific suggestions:

Do I want to change the delegated function name?

This is supported - renaming.

Should the forwarding functions be inline or not? Should they be marked with some other custom attributes?

Extra attributes can be added because delegation item is still an item, and items may have attributes.
inline is an open question (front matter).
It should probably be added by default, but overridable.

just enable delegating traits with some simple syntax.
From my experience at least, this is the most common use-case for delegation.

Yep, the statistics also show that it's common, just not overwhelmingly common, see the numbers in postponed features.
So we'll certainly try to do it, but it is postponed as a second layer of sugar, until we implement the basic layer.

  • Should their return values or input parameters be coerced somehow? Into, TryInto, AsRef etc.? Should the result be unwrapped?
  • Should additional expressions be passed to the forwarded methods?
  • Should await be called on the forwarded methods?

That all falls under arbitrary argument pre-processing and result post-processing and therefore not supported due to not fitting into syntactic budget for a built-in feature - rejected features, syntactic budget.

If some cases are supported by the same method as SameUpToSelfType then it's good (that may be possible for the From/Into/AsRef/AsMut-like stuff), otherwise not supported.
The data collection pass can actually be extended to check which specific kinds or pre- and post-processing are common.

I guess there is a general "workaround" for a case in which most of the method body can be delegated - make a private delegated function, and a real function that calls the delegated one and does the necessary pre-post-processing.

@petrochenkov

Copy link
Copy Markdown
Contributor Author

Regarding reflection, I'm interested in seeing proposals, but I'm not sure it's even implementable in a general enough form with Rust type system, and the readiness timeline will be 5-10 years at least.
Even this proposal with its signature copying may open sort of a can of worms. to which rustc is I'm not sure is ready. We'll see, I guess.

@Kobzol

Kobzol commented Nov 23, 2023

Copy link
Copy Markdown
Member

Regarding reflection, I'm interested in seeing proposals, but I'm not sure it's even implementable in a general enough form with Rust type system, and the readiness timeline will be 5-10 years at least.

Yes, sadly I also think that reflection is way off. So while I think that it would be a more general solution, if we want to get delegation faster, it makes sense to handle it specifically, and not wait for reflection.

@ericsampson

ericsampson commented Nov 23, 2023 •

Copy link
Copy Markdown

Yeah for sure.

Just for clarity, I am very excited that this is being worked on, so thanks a ton!!!

The RFC is well-written, and I really appreciate the data-driven approach.

Cheers 😊

Comment thread text/0000-fn-delegation.md Outdated
| Arg0Preproc.RefField | 136 | `&(mut) arg0.field` |

`self.field` is not even the most common transformation, usually the first argument is not
transformed at all, although delegation for static methods and free functions skews the statistics

@RalfJung RalfJung Nov 30, 2023 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's a "static method"? Do you mean associated functions without self? I don't think I have seen them called "static" in Rust before. They surely don't have anything do do with static which is a keyword in Rust after all.

I always thought that's what the method vs function distinction is about: methods have self, functions do not.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To my knowledge, "functions" is a group that is made up of the disjoint groups "functions that take self" and "functions that do not take self". The former are called "methods" and the latter has no short name, although some other languages call a similar grouping "static methods".

Notably, both fn foo() and fn bar(self) use the fn keyword, which AFAIK means "function".

@RalfJung RalfJung Nov 30, 2023 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, so "functions" is the union of "methods" and "not-methods" then, I see.

With a definition like

Associated functions whose first parameter is named self are called methods

I would argue a "static method" ought to be a kind of method, but a function that does not take self is not a method, so calling it a "static method" is very confusing. I was surely confused. ;)

Java calls them "static methods" but they also use the static keyword to declare them so that makes some amount of sense. Our static keyword has a different meaning though so IMO it makes little sense for us. In Java static methods are statically dispatched while regular methods are dynamically dispatched; that does not apply for us either.

@petrochenkov petrochenkov Nov 30, 2023 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's just my C++/Java past leaking, the terminology seems so natural I didn't realize it's not often used in Rust since it doesn't use static for this purpose.
Grepping the rust-lang/rust repo for "static method" shows that quite a few people stepped into the same issue though.

It's curious that you had nothing against newtypes, which are also never mentioned in the official docs :D

@SOF3 SOF3 Dec 1, 2023 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Java static methods are statically dispatched while regular methods are dynamically dispatched; that does not apply for us either.

Is that what static is supposed to mean? In Java, what affects whether the dispatch is dynamic would be final rather than static, and static not being dynamically dispatched is just a consequence of not having a receiver to dispatch on.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that what static is supposed to mean?

No idea, it's just my mental model. I can't otherwise explain why these would be called static methods.

But in Java they have to be static methods since Java only has methods, not functions. In Rust, calling them any kind of method makes little sense, you can't even use method call syntax to call them.

@RalfJung RalfJung Dec 1, 2023 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's curious that you had nothing against newtypes, which are also never mentioned in the official docs :D

That's a standard term across many language communities. It also doesn't involve any unrelated keywords or so that would make in unsuited as a Rust term.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The term "static method" also exists in many languages… probably in most OOP languages. Some of those languages also support functions that are not methods, such as C++, Python, and JavaScript.

In those three languages, a method is a function associated with a type, and a static method is a method that does not have a self/this argument.

None of those languages have anything like Rust traits, so the analogy is not precise.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No idea, it's just my mental model. I can't otherwise explain why these would be called static methods.

Historically speaking, Java got the term static from C++, and in C++ dynamic dispatch is not the default, requiring a separate keyword (virtual); so that can't have been the original motivation for the term. I have no idea why they picked static, but it probably helped that static was already a keyword in C, albeit with an unrelated meaning.

(In truth, both C and C++ are guilty of overloading static to mean several different things depending on context.)

@joshtriplett

joshtriplett commented Dec 5, 2023 •

Copy link
Copy Markdown
Member

If we end up doing data analysis of broader crates.io, I wonder how many of the ArgsPreproc.Other cases involve Pin? Delegating traits like AsyncRead/AsyncWrite currently sometimes requires invocations of Pin::new.

Those aren't likely to come up in your current data set, though.

@petrochenkov

Copy link
Copy Markdown
Contributor Author

Design question: how exactly the body template is duplicated for list or glob delegation

Typically the target expression (i.e. to what we delegate) in a delegation item is very simple,
like.

reuse a::b { self.field }
// or
reuse b::c { self.getter() }

However, it's possible that it may contain some entities with identity, like items or closures.
I would expect anonymous constants, closures and imports to be most useful in this context.

reuse prefix::{a, b, c} {
    use some::import; // import
    (
        self.field.map(|x| x.y), // closure
        [10; SIZE], // Anonymous constant
    )
}

This poses a question - how are all these entities duplicated when we desugar the list delegation
prefix::{a, b, c} or glob delegation prefix::* into multiple actual functions

fn a { ... }
fn b { ... }
fn c { ... }

Will there be 3 separate imports, closures and anonymous constants with their own identities,
or there will be only one instance, and the generated functions will refer to it?

Due to implementation details of rustc (how DefId tree is built), the answer dictates the
compilation stage at which the desugaring happens.

Alternative 1: Duplication at token / AST level

Every generated function will contain its own version of items/closures/constants/etc.

This is closest to what would happen if we wrote the function bodies manually instead of delegating.

Each item clone will have its own identity - its own DefId.
That means the desugaring of list and glob delegations into single delegations must happen before
definition collector runs, that means during macro expansion and import resolution, not even during
AST -> HIR lowering or right before it.

For glob delegation (reuse prefix::*) it means that prefix must be resolvable early, but that
seems fine because we are only going to realistically support glob delegation to traits
(reuse Trait::*).

Both list and glob delegation, including target expression cloning, essentially become macro
features.
That means we don't even need to parse the target block contents as Rust code if the delegation
list is empty (prefix::{} { random garbage tokens }), or if the glob refers to a trait without
items (EmptyTrait::* { random garbage tokens }).
It still probably makes sense to parse it (but not name-resolve it), then such code will be
equivalent to code under #[cfg(FALSE)] or any other code dropped by a macro.

Unfortunate detail: prefix still needs to be resolved for empty delegations, and therefore
stability checked as well. It means "delegation stems" will need to be preserved in HIR to reach
stability checking, similarly to import stems.

Drawback of this approach: name resolution will run multiple times on every copy of the target
block, even if there are no items inside it and all the results are the same.
Possible solution: better name resolution caching, not just for delegation, but for everything.

Alternative 2: "Semantic" duplication

Every generated function will contain references to items/closures/constants/etc canonically defined
in the single delegation item block.

Item definitions will be parented under DefId of the delegation item itself, not under DefIds
of functions that it produces.

I suppose this can work for imports, and other item definitions (e.g. structs).

However, I'm not sure how this is going to work for e.g. closures.
Type checking results for different functions generated from a delegation item may be different,
that's a significant design point.
Even in simplest cases different bodies may differ in their use Deref or DerefMut, for example.
Closure signatures are also produced by type inference, so in theory they may differ too, but that's
impossible if all the closures have a single identity.

With this approach the block body will be parsed and name resolved once, even if the delegation
item is "empty" and produces no functions.

This approach also seems to just difficult to implement, for no good reason, especially for things
like closures that are actually a part of the executable code, can capture local variables, etc.

Alternative 3: Prohibit everything with identity in target blocks

This is actually combinable with both Alternative 1 and Alternative 2, we can emit a hard error or
a feature gate for this case until a practical case requiring it arises.

However, we still need to make a choice between 1 and 2 because the implementation strategy depends
on it.
We also still need to decide how much checking is performed for target blocks or "empty"
delegations.

The choice

I suggest selecting the Alternative 1.

It does make delegation sort of a macro feature to a larger degree, but makes life easier in all
other regards, and keeps the "desugaring is identical to manually written code" property.

bors added a commit to rust-lang-ci/rust that referenced this pull request May 15, 2024
delegation: Implement list delegation

```rust
reuse prefix::{a, b, c};
```

Using design described in rust-lang/rfcs#3530 (comment) (the lists are desugared at macro expansion time).
List delegations are expanded eagerly when encountered, similarly to `#[cfg]`s, and not enqueued for later resolution/expansion like regular macros or glob delegation (rust-lang#124135).

Part of rust-lang#118212.
jieyouxu added a commit to jieyouxu/rust that referenced this pull request Jun 19, 2024
delegation: Implement glob delegation

Support delegating to all trait methods in one go.
Overriding globs with explicit definitions is also supported.

The implementation is generally based on the design from rust-lang/rfcs#3530 (comment), but unlike with list delegation in rust-lang#123413 we cannot expand glob delegation eagerly.
We have to enqueue it into the queue of unexpanded macros (most other macros are processed this way too), and then a glob delegation waits in that queue until its trait path is resolved, and enough code expands to generate the identifier list produced from the glob.

Glob delegation is only allowed in impls, and can only point to traits.
Supporting it in other places gives very little practical benefit, but significantly raises the implementation complexity.

Part of rust-lang#118212.
jieyouxu added a commit to jieyouxu/rust that referenced this pull request Jun 19, 2024
delegation: Implement glob delegation

Support delegating to all trait methods in one go.
Overriding globs with explicit definitions is also supported.

The implementation is generally based on the design from rust-lang/rfcs#3530 (comment), but unlike with list delegation in rust-lang#123413 we cannot expand glob delegation eagerly.
We have to enqueue it into the queue of unexpanded macros (most other macros are processed this way too), and then a glob delegation waits in that queue until its trait path is resolved, and enough code expands to generate the identifier list produced from the glob.

Glob delegation is only allowed in impls, and can only point to traits.
Supporting it in other places gives very little practical benefit, but significantly raises the implementation complexity.

Part of rust-lang#118212.
rust-timer added a commit to rust-lang-ci/rust that referenced this pull request Jun 19, 2024
Rollup merge of rust-lang#124135 - petrochenkov:deleglob, r=fmease

delegation: Implement glob delegation

Support delegating to all trait methods in one go.
Overriding globs with explicit definitions is also supported.

The implementation is generally based on the design from rust-lang/rfcs#3530 (comment), but unlike with list delegation in rust-lang#123413 we cannot expand glob delegation eagerly.
We have to enqueue it into the queue of unexpanded macros (most other macros are processed this way too), and then a glob delegation waits in that queue until its trait path is resolved, and enough code expands to generate the identifier list produced from the glob.

Glob delegation is only allowed in impls, and can only point to traits.
Supporting it in other places gives very little practical benefit, but significantly raises the implementation complexity.

Part of rust-lang#118212.
@petrochenkov

petrochenkov commented Jun 28, 2024 •

Copy link
Copy Markdown
Contributor Author

Design question: what does the block around the target expression mean?

Suppose we have two delegation items

reuse just_expr { self.0 }

reuse multi_statements {
    let x = something;
    self.get(x)
}

What is the meaning of the curly braces around self.0 - is it actually a block expression or just a syntax that looks like a block expression.
In the just_expr case it's likely the latter, and in the multi_statements case it's likely the former (but there are nuances).

Single expression

It's clear that if we implemented some different syntax, e.g. reuse PATH from EXPR, then the just_expr example would be written as

reuse just_expr from self.0;

and not as

reuse just_expr from { self.0 };

The block expression here would be not just noisy, but harmful in the common case.
For example, the body generated for this code could fail borrow checking (autoref/deref is assumed).

just_expr(&{ self.0 }) // ERROR cannot move out of `self`

Multiple statements (alternative 1)

The second example with the alternative syntax would look like this.

reuse multi_statements from {
    let x = something;
    self.get(x)
}

And the generated body would look like this.

multi_statements({ let x = something; self.get(x)})

The block expression is clearly necessary here.

Multiple statements (alternative 2)

An alternative body desugaring for the multi-statement case could be

let x = something;
multi_statements(self.get(x))

This could potentially be better for borrow checking, but it needs to be proven with some practical cases.

The catch is that it no longer fits into some alternative expression only syntax like reuse PATH from EXPR, because the block here is clearly not a block expression.
If we need to actually delegate to a block expression we will need "double blocking" reuse foo { { bar } }.

The choice

Not clear yet.
The current implementaion uses the "alternative 2" desugaring, but strips the block if it contains only a single expression, which is a bit hacky.

We may strongly consider changing the defaults here, and avoid the semver hazards and behavior different from existing features like derives.
This will also make the `reuse impl` syntax usable in more cases.

In the future we may introduce a different flavor of glob delegation (with an attribute or first-class syntax), that includes methods with default bodies, and explicitly opts into potential breaking changes.

@Kixunil Kixunil Oct 23, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be too bad to just ignore non-delegatable methods that do have defaults?

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking that a method is non-delegatable is equivalent to generating the delegated function body and looking whether it fails type checking or not.
It's probably too subtle to switch the function to something else if the type checking silently fails.
Also it may be challenging to do the type checking speculatively and never report the errors, although I'm not entirely sure here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's yet another motivation for what I proposed in the past: stabilize stability attributes and then pretend that the API newer than what Cargo.toml declares doesn't exist. In this case it would omit delegating new methods.

So if crate a defines:

pub trait Foo {
    #[stable(since = "1.0.0")]
    fn foo(self) -> u32;
}

and crate b has foo = "1.0.0" in Cargo.toml`

then when a later changes to this, bumping the version to 1.1.0:

pub trait Foo {
    #[stable(since = "1.0.0")]
    fn foo(self) -> u32;

    #[stable(since = "1.1.0")]
    fn bar() -> u32 { 42 }
}

The bar method is ignored for delegation and there are no silent unexpected changes - when b upgrades to newer version of a only then the API changes. Though, it would be even better to have additional field max_tested_version which allows overriding this if the configuration is known to work but the newest version is not required for the crate to function (useful for MSRV stuff).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glob delegations are now changed to not include methods with default bodies.

The delegation feature could potentially be implemented as a third-party library with compile‑time [_reflection_](#reflection) (if and when that becomes available).

TODO: collect remaining drawbacks from the rationale sections.
Language features reducing the amount of boilerplate code are less important now when majority of code is read and written by LLMs.

@traviscross traviscross Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not to say you have to change it — it's your opinion, after all — but I tend to believe the opposite is true.

There's an upper bound on the number of characters of code that I can review per day, and agents can easily exceed that. The closer the code is to pseudocode, i.e., the better that it can express only the key ideas and the essential complexity rather than boilerplate and accidental complexity, the better that I can meaningfully understand, shape, and review the work.

So, in this new age, it may be true that abstractive power is even more important.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't necessarily agree with this point myself, just been collecting potential arguments against the feature.

@bushrat011899

Copy link
Copy Markdown

Not sure if this is the ideal place to mention this, but I just hit a funny issue here with fn_delegation as it's currently implemented. If you reuse Iterator::*, but one of the Iterator methods is deprecated, you'll fire off a deprecation warning. Is that reasonable? If you wildcard use some_module::* and there's any deprecated items in said module, you don't fire off a warning until you explicitly use the particular items in question.

If this is expected, I'd worry about allow(deprecated) almost being required to delegate to upstream traits in a non-breaking way.

@petrochenkov

Copy link
Copy Markdown
Contributor Author

Not sure if this is the ideal place to mention this, but I just hit a funny issue here with fn_delegation as it's currently implemented. If you reuse Iterator::*, but one of the Iterator methods is deprecated, you'll fire off a deprecation warning. Is that reasonable? If you wildcard use some_module::* and there's any deprecated items in said module, you don't fire off a warning until you explicitly use the particular items in question.

If this is expected, I'd worry about allow(deprecated) almost being required to delegate to upstream traits in a non-breaking way.

The lint can always be tweaked to not report globs.
But otherwise the problem should solve itself if we switch to not including functions with default bodies into glob delegation.

Comment thread text/0000-fn-delegation.md Outdated

↩ [_Glob delegation_](#glob-delegation)

#### Why are methods with default bodies included in glob delegation?

@programmerjake programmerjake Oct 29, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Continuing from #3530 (comment):

But otherwise the problem should solve itself if we switch to not including functions with default bodies into glob delegation.

some traits such as Iterator have methods that are often waay more efficient when they call the inner iterator's method instead of relying on the default, e.g. Iterator::nth can be $O(1)$ on many iterators but the default is $O(n)$. there are also methods you can't really override because they call a private API, e.g. Iterator::map.

That all makes me think there should be an attribute to set whether or not a function should be included in a glob delegation, so Iterator::nth can be included but Iterator::map excluded.

View changes since the review

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps if the type you're inheriting the methods from overwrites the default for a trait it should be forwarded to the type? And otherwise, use the default? In both cases, I would expect a deprecated method to be silently allowed to be forwarded, but the deprecation attribute would also be forwarded.

@petrochenkov petrochenkov Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps if the type you're inheriting the methods from overwrites the default for a trait it should be forwarded to the type?

Unfortunately, this is only known very late during compilation, during monomorphization perhaps.

The attribute approach looks reasonable - we do not glob delegate to methods with default body by default, but if there's an attribute like #[glob_delegate_me] fn method() { /*default body*/ }, then we do.
And then it's responsibility of the API author to only add this attribute in non semver breaking ways.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also a pretty reasonable alternative to my proposal above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glob delegations are now changed to not include methods with default bodies, and the #[delegate_me] attribute is listed as a future possibility.

@petrochenkov petrochenkov added the I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. label Nov 3, 2026
@petrochenkov

petrochenkov commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

@traviscross (since you read the RFC), could we get some "prioritization decision" from lang team about this RFC?
I think for us it would be enough if the decision was one of

  • "We'll decide whether to accept or reject it soon (in 2026)."
  • "We'll postpone the decision to 2027."
  • "We'll postpone the decision to 2028+, or when reflection is ready, or something like that."

(This work was funded, and putting something more definitive than "the RFC will probably be considered by lang team eventually" into the final report will increase our chances of getting further funding for Rust-related work.)

@traviscross traviscross added I-lang-radar Items that are on lang's radar and will need eventual work or consideration. P-lang-drag-1 Lang team prioritization drag level 1. labels Nov 4, 2026
@traviscross traviscross removed I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. P-lang-drag-1 Lang team prioritization drag level 1. labels Nov 11, 2026

### Name-based resolution as sugar

A shorter syntax that infers the callee from a bare method name could be layered on top of fully qualified paths.

@scottmcm scottmcm Nov 11, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually rather like having a somewhat-restricted rule here, to avoid semver traps.

I think I'd rather have this via "that's an error but we used name resolution to give you a structured fix with the full thing".

AKA I don't want reuse coalesce and would much rather people do reuse Itertools::coalesce so we can still add Iterator::coalesce.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "delegate by method name rather than path" approach initially came from the previous RFCs, but gradually went into the background as implementation for this RFC progressed.
Now I'd personally prefer to just settle on paths and never go in that direction.

pub(vis) reuse prefix::{a, b, c as d} { target_expr }
```

The motivation here is to avoid more complex features such as argument or return-value transformations, which would require preprocessing or postprocessing closures. With additional bells and whistles like that, a delegation item becomes as verbose as a full forwarding function implementation, but less readable. These transformations can instead be written manually or expressed using a macro (see [_Prior art_](#prior-art)).

@scottmcm scottmcm Nov 11, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wanted to 👍 this. At some point -- especially with good structured fixes -- the answer is just to write the function yourself. So delegation only handling a subset of the "things I would think about as delegation" is absolutely right. (Making it super-complicated to be able to handle everything would be a worse feature.)

View changes since the review


Some delegation subfeatures, such as delegation to inherent methods, may work in a limited way, since supporting them properly would require compiler reengineering to avoid query cycles. Some of these limitations are discussed throughout the proposal.

## Guide-level explanation

@scottmcm scottmcm Nov 11, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a bunch in here about things that you could do. I think it'd be nice to also have, say, an "advice" section or something of how you should use it.

For example, when should I delegate PartialOrd instead of deriving it? Is that ever a good idea?

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a derive exists and generates the right thing (e.g. doesn't produce overly conservative bounds), then, I think, the derive is always more convenient and preferable.
I'll mention it in the RFC.

@scottmcm

Copy link
Copy Markdown
Member

Excited to see progress on this!

As I was reading through and from the discussion in the design meeting, I was pondering a few interactions.

final fn

This one's probably trivial, but I didn't see it mentioned. If we have final fns in a trait, the glob reuse obviously needs to not forward it.

Default field values

If I'm delegating to self.a in a type like

struct Foo {
    a: String,
    b: u32 = 4,
    c: Option<char> = None,
}

then should the wrapping be instead to Foo { a: blah } and thus not compile or to Foo { a: blah, .. } so it does?

Probably it should only work with an explicit { self.a }?. I agree with the PhantomData note that it should never use a "first field" rule.

Interactions with Derives

Should we have single-field derives emit delegation instead?

Today we don't emit lt/le/gt/ge in the derive for PartialOrd. But for a newtype id around u32, it might actually be better to have those instead of forcing the compiler to optimize them from partial_ord.

Would it thus be better for that derive to work via reuse impl instead when it sees a single-field struct? Can that get us any advantages around not needing to emit all the code for it (that would need to be parsed and such)? Or does the delegation actually end up emitting all the same code that the derive would anyway and thus actually delegating all those would be a perf hit normally?

Are there any places where we'd tell people to delegate a derivable trait?

Delegation of empty traits

If I reuse impl Eq to a float field, does that just work? Should it?

Interactions with stability

If I glob-delegate in an inherent impl block, does that pick up unstable methods? How does stability checking work with that? Is it an error at delegation time if the feature is off? Does the glob not pick it up? Does the stability get copied over?

How do we report errors

Suppose it doesn't work. How do we report errors in an understandable way? How often can things be reported in the attached block directly -- obviously things like a typo'd field could be -- and how often is the error only in something that only makes sense with extra context? Imagine there's a borrow checker error that's dependent on the interaction between the delegation block and the wrapping of the return value: will we be able to show it in a way that the user can actually understand what happened?

Interaction with trait splitting

We have a lang/libs goal that we'd like people to be able to split up a trait into two without that being a breaking change. What features does that require delegation to have in order for such a splitting to continue working?

(It seems like sometimes the glob might need to pick up methods from multiple traits, at least?)


Apologies for any dups in here that are already addressed.

@petrochenkov

Copy link
Copy Markdown
Contributor Author

@scottmcm

final fn

Final functions have default body, so this case will also be addressed by switching to not glob-delegating to methods with default bodies.

Default field values

https://github.com/petrochenkov/rfcs/blob/bca9da465d8eca18c853f0ecc48f92035c92527f/text/0000-fn-delegation.md#why-is-return-type-wrapping-restricted-to-single-field-structs

Should we have single-field derives emit delegation instead?
Or does the delegation actually end up emitting all the same code that the derive would anyway and thus actually delegating all those would be a perf hit normally?

Delegation will emit all the same code, so I don't see much reason to do it.
IIRC, someone measured that most overhead from derives comes from typechecking and codegening the generated code, not from parsing or running the macros themselves.
With delegation it should be similar.

Are there any places where we'd tell people to delegate a derivable trait?

When derive generates overly conservative bounds perhaps?

#![feature(fn_delegation)]

struct RcWrapper<T> {
    field: std::rc::Rc<T>
}

// Avoids the `: Clone` bound on `T`, unlike derive.
reuse impl<T> Clone for RcWrapper<T> { self.field }

Delegation of empty traits
If I reuse impl Eq to a float field, does that just work? Should it?

Empty glob delegation is an error, as specified in the RFC.

The catch is that Eq actually has some hidden methods with default bodies, so it currently works, similarly to manual impl Eq for FloatWrapper {}.
It will stop working when we switch to not glob-delegating to methods with default bodies.

Interactions with stability

Delegation ignores stability, and glob-delegation will pick up unstable methods, and will produce items without any stability annotations, the stability is not copied over.
If delegation uses some unstable method without a feature, it will be a stability error.

We'll probably need a lint for delegating from stable items to unstable items, similarly to delegating public items to private items.

How do we report errors in an understandable way?

Currently we do not, as mentioned in the "Drawbacks" :)
We'll probably need to pretty-print the generated body and map errors from type checking and other passes onto it.

@petrochenkov

Copy link
Copy Markdown
Contributor Author

Interaction with trait splitting

So if we change

trait Trait {
  fn base();
  fn method();
}

into something like

trait Base {
  fn base();
}
trait Trait: super Base {
  fn method();
}

then (glob) delegations like reuse impl Trait for Wrapper { self.inner } should continue compiling (easy) and continue implementing both of the traits (harder)?

I think glob delegations should be able to fetch methods from specially marked super traits, yes.
It would be a bit unfortunate to not be able to delegate Trait and Base separately after the split (because impl Trait ... would delegate both at the same time), but we should be able to keep the code written before the split working at least.

@petrochenkov

petrochenkov commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor Author

@scottmcm
Looks like I never actually answered https://hackmd.io/Yc0fQGXBRnSLGUdXlmJowA#What-are-the-rules-for-unwrapping-and-wrapping (or rather answered a different question).

It should be covered by this section https://github.com/petrochenkov/rfcs/blob/bca9da465d8eca18c853f0ecc48f92035c92527f/text/0000-fn-delegation.md#why-are-effective-self-types-not-detected-through-type-aliases-or-type-equality and the corresponding reference section.

For reuse impl Add specifically we only know that the second operand is an effective self parameter (and therefore should be remapped) if there's no explicit generic argument to Add and the Self type parameter is used as the default.

This branch has not been deployed

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

Labels

I-lang-radar Items that are on lang's radar and will need eventual work or consideration. T-lang Relevant to the language team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.