-
Notifications
You must be signed in to change notification settings - Fork 12.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
TAIT defining scope options #107645
Comments
Discussing T-lang triage. We want/need to resolve choice between options provided asynchronously. After we come to some conclusion, we'll FCP merge/close this to demarcate that conclusion. |
Here's the zulip thread the T-lang team will use to align on this: https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/align.20on.20TAIT.20defining.20scope.20options.20lang-team.23107645/near/329277776 |
This comment was marked as outdated.
This comment was marked as outdated.
@rfcbot fcp merge I propose that we accept #107809. It implements a conservative path forward. Basically any function that constraints a TAIT but doesn't list the TAIT in its arguments/return type is a hard error, giving us room to change the behavior in the future. Final behavior as I understand it
Current bugs and limitations (forwards compatible to change)
@rustbot labels -I-lang-nominated |
Team member @nikomatsakis has proposed to merge this. The next step is review by the rest of the tagged team members: Concerns:
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! cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns. |
Not that I know of, though it may feel like a very artificial limitation to users |
We allow |
I am fine with adding a restriction to return position. I think it covers the major use cases (though perhaps @Dirbaio may wish to weigh in? I recall Embassy making creative use of TAITs). I also like the story of "you can now take an But to me the biggest detail is that we can easily allow more positions, but it's hard to take them back. |
It's not entirely true, is it? In |
I would prefer to allow it anywhere in the argument or return types. What would be the rationale for limiting it to just return types? |
it can, but you have to write type Foo<T> = impl Trait;
fn foo<T>(x: T) -> Foo<T> { ..} |
in Embassy, the user writes this: #[embassy_executor::task]
async fn run() {
// stuff
} which gets expanded to: async fn __run1_task() {
// stuff
}
fn run1() -> ::embassy_executor::SpawnToken<impl Sized> {
type Fut = impl ::core::future::Future + 'static;
static POOL: ::embassy_executor::raw::TaskPool<Fut, 1usize> = ::embassy_executor::raw::TaskPool::new();
// defining use here. Signature for `_spawn_async_fn` is:
// impl<F: Future + 'static, const N: usize> TaskPool<F, N> {
// fn _spawn_async_fn<FutFn>(&'static self, future: FutFn) -> SpawnToken<impl Sized>
// where FutFn: FnOnce() -> F;
// }
// so it constrains `Fut` to be the future for `__run1_task()`.
unsafe { POOL._spawn_async_fn(move || __run1_task()) }
} Honestly, I'm having a very hard time figuring out whether this'll be allowed in the new rules from reading #107645 (comment) ... Here's my thought process:
Okay, so the defining scope is
The defining use here is an expression. Are expressions "items"? They aren't, according to this? Either way, even if expressions are items, that expression wouldn't be a "defining use" because it doesn't contain such a function. (btw, "item contains a function that lists the TAIT" is very confusing. This means the item can be an impl or a mod that contains the function, it doesn't need to be a function itself? The function can be nested 100 mods deep, or does it have to be an immediate child?)
The expression is constraining, but if it's not an item then it's not a "constraining item"...???
Again, is an expression a "constraining item"? if yes, then I guess Embassy's use is no longer allowed?
This seems the same as before, so we should be good here. The previous rules around defining scopes (the ones in nightly currently) were already confusing enough, but the new ones are way way worse. They seem incredibly complex to me. I consider myself an experienced Rust user, and yet I can't figure out how to apply them to this case. IMO Rust should prioritize user-friendliness over IDE/compiler-friendliness. This seems the wrong tradeoff to me. |
Why was The previous "defining scope is the parent item of the TAIT" rule already felt very artificial to me. The new "defining scope is any function that's child of the parent item of the TAIT, that mentions the TAIT in the return type after normalization" rule feels even more artificial. While with
It's slightly more verbose, sure. I don't think that's a problem. TAIT is a rather advanced feature |
I disagree, in previous experiments I've found TAIT to be really helpful when doing trait implementations in applications, things like |
Okay, I agree the "user vs library code" is not a very strong argument. The other arguments still stand: it's not that verbose, and it massively reduces complexity and implicit stuff. This is not that verbose: impl IntoIterator for Foo {
type IntoIter = impl Iterator<Item = Self::Item>;
#{defines(Self::IntoIter)}
fn into_iter(self) -> Self::IntoIter {
...
}
} perhaps it can be made less verbose too, though I don't like the idea having 2 syntaxes for the same thing: |
yeah, but why? Why do we need such complicated rules? |
Regarding Example: fn items. Function items of course have a a defining scope of the function (and this is stable): fn foo() -> impl Bar { } // defined by this function Example: impl assoc types. This stabilization includes (I believe) impl Foo for Bar {
type IntoIter = impl Baz; // defined by methods in this impl (which return this assoc type)
} Example: const or static. Embassy relies on const Foo: impl Debug = 22; Example: TAIT. The question then is what happens when we extend to a TAIT like the following type Foo = impl Debug; The current proposal continues with "defining scope is the enclosing item", and in this case the rule is this Including defines would break the pattern. I think there's a decent case for that, basically the argument is that top-level items are different. These are the things that can be "used" and imported and so forth. Potentially true, although the idea of having Alternative: defines for TAIT. The proposal would then be: if you define a TAIT (as opposed to an Multiple opaque types. It's worth pointing out that there is not a 1:1 relationship between a type name like type Bar = (impl Debug, impl Debug); but I suppose that saying One consideration, which I don't think is a particularly big deal, is that you can't extract a type-alias for an fn foo() -> impl Debug { }
// becomes
type Foo = impl Debug;
#[defines(Foo)]
fn foo() -> Foo { ] This doesn't seem like a big deal because extracting |
@Dirbaio see above. I appreciate the pushback. I could get behind a version like so. Sugary cases: fns/impls/consts/statics
Explicit case: type aliases.
I like that it is possible to precisely desugar all the "sugary" cases to a TAIT that precisely matches their semantics. I also like that behavior with auto traits and other things is much easier to explain. |
Some more in-depth discussion of the issues with the current defining scope heuristic and the pros and cons of making it explicit with something like |
After much helpful discussion with @traviscross and @compiler-errors, I'm resolving my outstanding concerns. @rfcbot resolve nested-modules-can-always-define-but-nested-functions-cannot I'm not too concerned about this inconsistency, and only ever wanted some reason it should be this way. I believe the reason was that tools such as rust-analyzer would want to be able to skip parsing function bodies. On the other hand, it seems likely that we will change it anyway, given its interactions with rules recently proposed by @rust-lang/types. @rfcbot resolve encapsulation-is-too-powerful My reason for resolving this concern is the combination of the following:
I feel a bit weird about basing a decision on so many factors I consider necessary for that decision, but it seems like this is just a complex problem space. I'm laying them out here so I (and others) can refer back to them. With all that said, we do need another t-lang meeting before stabilizing TAIT. I've asked for the future possibility of Footnotes
|
🔔 This is now entering its final comment period, as per the review above. 🔔 |
This is not true at all. We did a survey of code in the wild a while ago on Zulip with @traviscross, and the result was that the "parent mod + signature restriction" works about 50% of the time, and requires refactors, dummy modules or dummy functions otherwise. i believe the proposed "functions meeting the signature restriction MUST constrain" change will reduce the percentage further. works: user adds TAIT to existing code, the restrictions pass, everything compiles.
fails: TAIT doesn't work out of the box, user must do dummy modules/functions or move code as a workaround.
Note some of these are from before the signature restriction landed on nightly, so just the "parent mod" restriction was already causing problems. Also note there is a very strong "survivorship bias" in searching GitHub. When TAIT doesn't work, users are likely to fall back to using
TAIT doesn't have to block the RPIT capture rules. There's the |
Thanks @Dirbaio for taking the time to provide a counterpoint. Regarding the examples you helpfully provided in the second list, I observe that 4 out of the 6 on the list I can review are due to cycle errors or frustration with cycle errors.1 We all agree that spurious cycle errors are annoying and have been a pain point, and we're eliminating those as part of this plan. Under this plan, with the new trait solver, there will be zero spurious cycle errors regardless of how the code is factored. The details of the plan have been carefully chosen to ensure this. As for the remaining two examples, Example 5 would either need the opaque added in a type Led = impl OutputPin;
fn to_led(x: Output) -> Led { x }
// ...
let led = to_led(Output::new(..)); In some contexts (perhaps not this one), it would be reasonable for such an The way that Embassy uses TAIT as an alternative to the newtype pattern is not really what people had in mind when designing TAIT. It's a surprising benefit that it does in fact work for this use-case also with some simple patterns such as the Regarding Example 7, that one is difficult to analyze. That's deep in some code-generating code for RTIC. It wouldn't surprise me if code-generating code had to more consciously accommodate the restrictions, as such code often has to do. Again, thanks for the interesting examples and for providing a counterpoint. Footnotes
|
The proposed solution to eliminate the cycle errors as far as I understand from the meeting minutes is "if function meets the parent mod + signature restrictions, it MUST constrain the opaque" (vs currently it MAY constrain). Cycle errors currently happen when a function
With that proposed solution, such code will still be rejected (because the fn passes the restrictions but doesn't constrain). The change does improve the error message, which is nice, but doesn't make these cases work. The user still has to refactor their code, like moving that function outside the TAIT's parent module, or make a dummy module to reduce the defining scope.
Dummy
The whole point of using TAIT there is to avoid having to write out the full type. These workarounds require writing out the full type, so they completely defeat the point. In that example the full type is All these examples could use TAIT if we had
The use case is not "alternative to the newtype pattern". It's letting the compiler infer long complex types so I don't have to type them out by hand. And how is that use case not intended by the original TAIT design? Quoting from the original TAIT RFCs: RFC 2071, impl Trait existential types:
That's exactly what I want to do, pass a complex type without having to write it out manually. RFC 2515, Type Alias Impl Trait:
Again, that's exactly what I want to do. Declare a type alias (i.e. a simpler name) for a type while letting the compiler infer it for me, so I don't have to type it out. These use cases I'm bringing up fit perfectly with the original goals of TAIT. Yet the current design can't make them work.
Again: no, they don't work, because the goal of using TAIT here is avoiding writing out the full type. |
That's correct, but I understand that this restriction can be relaxed once the new trait solver lands (or we fix the old one enough to be confident that switching to the new solver won't break existing code). I'm wondering if it's feasible to let functions that do not constrain continue to compile under a feature flag, with the expectation that some instances of the pattern will require refactoring in the future. I think @compiler-errors or @oli-obk would know. |
Thanks @Dirbaio for taking the time to make a detailed reply. I'll try to fill in some context below.
The purpose of this restriction is to preserve forward compatibility with the new trait solver. Once the new trait solver lands, this restriction could be lifted and there would still be no spurious cycle errors. By lifting this restriction, we would be able to accept the kind of code that you mention as being desirable to accept without refactoring.
Setting aside stylistic questions about this and focusing just on feasibility (as it may be better or more idiomatic to use other patterns here in any case), there are options to address the points you raise using patterns already common in Rust. While type MyTait = impl Sized;
fn main() {
fn inner() where MyTait: Any {
let _: MyTait = ();
}
inner();
} The inner function pattern is also common in Rust for many other purposes. Using inner functions in this way would also prevent the (But again, it's probably not worth debating this point too much, as other patterns are likely to be superior overall.)
People can disagree about what the purpose of an RFC was, and that's OK. Whether or not the use case you're describing was the original purpose, I think it's great that we can support it with some common patterns. However, my reading of the RFCs is a bit different than yours. The original design intent, as I understand it, was to allow naming types that currently cannot be named, and also to allow interfaces like the one It wasn't, in my understanding, intended as a generalized mechanism for letting the compiler infer types so they don't have to be written out. In fact, people (including you, if I recall correctly) have previously proposed non-opaque versions of TAIT (or similar) that would better address this use case. But there has not so far been appetite for this for various reasons. Where this distinction matters is that for the intended use cases like Again, thanks for taking the time to engage so thoughtfully with this. |
This is just another workaround. (A workaround for issues caused by the first workaround!) Also, it should be on the user to decide if they want to do the "inner main" pattern, it shouldn't be forced on them by the language.
The RFC LITERALLY says "unnameable or complex types".
I disagree, there is A LOT of appetite for cross-function type inference (for either unnameable, or merely "complex" types). I think the links I've provided on my previous post of people trying to do that proves this beyond any doubt. |
We could say this about many patterns in the language. E.g., people use the inner function pattern to reduce the size of generated code due to generics. Is that actually a workaround? Maybe. We accept this in many cases as reasonable.
Specifically, RFC 2071 says:
As mentioned, people can disagree about the interpretation of text, and that's OK. My reading is that the authors used the word "complex" here so as to include
There has been limited appetite among the lang team. But it's reasonable to point out the possible presence of appetite in a larger sense, so thanks for doing that so we can distinguish these. |
I'm surprised at your position that these use cases are not intended to be solved by TAIT. Why wasn't this brought up in our previous discussion, 4 months ago? (this Embassy issue is from after that discussion, but we did discuss the other examples from embedded code, and they share the same characteristics). IMO it'd be a shame to leave these use cases unserved by TAIT, considering they are served well if we do |
My position is that the original RFCs did not intend TAIT as a generalized mechanism for letting the compiler infer types so they don't have to be written out.
We did discuss this at that time. E.g.:
(We've discussed many things. It's understandable to forget such details.)
In all of the examples we've discussed, the users were able to solve the problems they set out to solve using TAIT. The biggest pain point in the past that has put an asterisk on that has been spurious cycle errors, and we're eliminating those. |
Hi all, please be mindful of subscribers of this issue, and to keep discussion on-topic and moving (rather than hammering on the same points over and over again). @Dirbaio I'm sympathetic to your plea to make TAITs applicable to all the use cases people want for them out-of-the-gate. However, at the end of the day, getting TAITs stabilized to cover 50% of use cases for them without preventing future expansion to the remaining 50% is much better than delaying stabilization on building not just consensus an alternative, but implementing that alternative. If you have new thoughts or data to share on this, I'm happy to discuss on Zulip. |
I've collected all my thoughts on TAIT defining scope design in a document. It goes through the possible designs and pros and cons of explicit ( https://hackmd.io/i-xpzf-LR7q75pSRTDVSaA zulip thread: https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/TAIT.20defining.20scope.20implicit.20vs.20explicit |
The final comment period, with a disposition to merge, 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. This will be merged soon. |
I don’t think @Dirbaio points were properly addressed? Zulip threads seems dead and I don’t see a proper argument against points in Hackmd, only clarifying questions/answers. |
…mpiler-errors Split tait and impl trait in assoc items logic And simplify the assoc item logic where applicable. This separation shows that it is easier to reason about impl trait in assoc items compared with TAITs. See https://rust-lang.zulipchat.com/#narrow/stream/315482-t-compiler.2Fetc.2Fopaque-types/topic/impl.20trait.20in.20associated.20type for some discussion. The current plan is to try to stabilize impl trait in associated items before TAIT, as they do not have any issues with their defining scopes (see rust-lang#107645 for why this is not a trivial or uncontroversial topic).
…mpiler-errors Split tait and impl trait in assoc items logic And simplify the assoc item logic where applicable. This separation shows that it is easier to reason about impl trait in assoc items compared with TAITs. See https://rust-lang.zulipchat.com/#narrow/stream/315482-t-compiler.2Fetc.2Fopaque-types/topic/impl.20trait.20in.20associated.20type for some discussion. The current plan is to try to stabilize impl trait in associated items before TAIT, as they do not have any issues with their defining scopes (see rust-lang#107645 for why this is not a trivial or uncontroversial topic).
…mpiler-errors Split tait and impl trait in assoc items logic And simplify the assoc item logic where applicable. This separation shows that it is easier to reason about impl trait in assoc items compared with TAITs. See https://rust-lang.zulipchat.com/#narrow/stream/315482-t-compiler.2Fetc.2Fopaque-types/topic/impl.20trait.20in.20associated.20type for some discussion. The current plan is to try to stabilize impl trait in associated items before TAIT, as they do not have any issues with their defining scopes (see rust-lang#107645 for why this is not a trivial or uncontroversial topic).
Rollup merge of rust-lang#119766 - oli-obk:split_tait_and_atpit, r=compiler-errors Split tait and impl trait in assoc items logic And simplify the assoc item logic where applicable. This separation shows that it is easier to reason about impl trait in assoc items compared with TAITs. See https://rust-lang.zulipchat.com/#narrow/stream/315482-t-compiler.2Fetc.2Fopaque-types/topic/impl.20trait.20in.20associated.20type for some discussion. The current plan is to try to stabilize impl trait in associated items before TAIT, as they do not have any issues with their defining scopes (see rust-lang#107645 for why this is not a trivial or uncontroversial topic).
during the stabilization of TAITs (type alias impl trait) in #63063 (comment) a concern was raised: it's not obvious for the compiler (and IDEs), which items' bodies are allowed to register hidden types for TAITs. For RPIT (return position impl trait) it was obvious: the body of the function in whose return type the
impl Trait
was in. For TAITs the status quo is point 1 in the list of options below. This may require some interesting module juggling to avoid cycle errors that can occur due to revealing the hidden type to proveSend
orSync
bounds, and that revealing again looking at the item to look for things registering the hidden type (see example and passing example).The possible schemes we know about currently are:
where
bounds is allowed to register the hidden type. this change has a prospective impl atwhere
bounds to register hidden typeswhere
boundswhere
bounds to register hidden typesRuled out schemes:
#[defines(NameOfTheTypeAlias)]
attribute is allowed to register the hidden typeI am nominating this issue for T-lang to discuss which options they prefer.
The text was updated successfully, but these errors were encountered: