Make trait methods callable in const contexts
Please remember to create inline comments for discussions to keep this RFC manageable and discussion trees resolveable.
Related:
- https://github.com/rust-lang/rust/issues/67792
I love it and I’m very excited to get to replace the outlandish const fn + associated const ~~hacks~~workarounds I’ve joyfully come up over the last years :D
Thank you for all of the hard work that everyone working on the various implementation prototypes and around has put into const traits!
I didn’t see const implementations in alternatives. Is there a reason they can’t be considered ?
Eg. const impl PartialEq for i32 { … } where only this impl is const, no changes to the trait.
In this proposal const fn foo<T: ~const Bar>() is a function that is const when Bar is const impl'ed for T. This doesn't feel very consistent to me, as const fn implies to me that the function is always const. Likewise, I don't like that const trait Bar { ... } is a trait that may be const impl'ed, but isn't required to be const. Here's what I propose as a way to make this all feel more consistent (in my opinion!)
-
constmeans something must beconstunder all conditions. -
~constmeans something will beconstunder certain conditions. - Without
constor~const, a signature implies non-const, while a bound implies indifference toconst.
Below is my justification for this belief. The tl;dr is I believe the syntax would be more consistent, and allow for sensible implied clauses without sacrificing API clarity.
// Function is const under all circumstances.
// This implies T: const Bar and S: const Baz
const fn foo<T: Bar, S: Baz>() { ... }
// Function is const if all trait implementations are const.
// This implies T: ~const Bar and S: ~const Baz
~const fn foo<T: Bar, S: Baz>() { ... }
// Function is not const.
// T could have a const or non-const implementation of Bar, likewise for S and Baz
fn foo<T: Bar, S: Baz>() { ... }
Details and examples
Applied to trait definitions:
// Trait is always const and must be implemented const
const trait Foo {};
// Trait may be const if it is implemented const
~const trait Foo {};
// Trait is never const
trait Foo {};
Users would then implement the trait using one of three blocks:
// The implementation is always const
impl const Foo for Bar {}
// The implementation may be const if possible (see below for discussion on this point)
impl ~const Foo for Bar {}
// The implementation is never const
impl Foo for Bar {}
When applied to bounds on functions, we get a side-effect in that the bounds on the trait can be entirely elided:
// Function is always const, therefore all trait implementations must always be const
const fn baz<T: Foo>() {}
// Function is const if all trait implementations are also const
~const fn baz<T: Foo>() {}
// Function is never const
fn baz<T: Foo>() {}
Users can still opt to use this proposal's trait bounds syntax where the requirements on a trait are stricter than the context of the signature:
// Non-const function requires that T has a const implementation of Foo
fn baz<T: const Foo>() {}
// Non-const function passing T into a ~const function. Allows compiler to use the const implementation if applicable
fn baz<T: ~const Foo>() {}
// Maybe-const function requires that T has a const implementation of Foo
~const fn baz<T: const Foo>() {}
This raises the question, how do you opt out of the implied const bound? I believe you'd never need to. Imagine T: !const Foo means means an implementation of Foo which is not const:
// ERROR: Non-const trait in const context
const fn baz<T: !const Foo>() {}
// Will never be const, so equivalent to fn baz<T: Foo>() {}
~const fn baz<T: !const Foo>() {}
// Seems pointless to me? Why would you want to declare code can't be const? Discuss!
fn baz<T: !const Foo>() {}
@bushrat011899 (and others who want to comment too): You'll probably get more success if you follow oli-obk's advice and use an inline comment so it can be threaded correctly and resolved when it's reached its mileage. I highly recommend you do that (and probably hide your original comment to dedupe it) just so people can reply to it more ergonomically.
I'll copy this if this ends up getting threaded.
// Function is const under all circumstances.
// This implies T: const Bar and S: const Baz
const fn foo<T: Bar, S: Baz>() { ... }
This is literally impossible to implement backwards compatibly. You can already write const fn foo<T: Bar, S: Baz>() today, and that doesn't require const impls for Bar or Baz.
The RFC doesn't mention which traits in the standard library will be constified. I guess the goal is to constify all of them, but it might not be possible to constify traits that use unsupported features, e.g. heap allocation.
This is due to the main limitation of this proposal: To constify a trait, all methods have to be const. This also means that marking a trait as const is a big comment, since after that you can't add non-const default methods backwards compatibly.
I particularly care about Iterator, which has a lot of default methods, and has a big impact due to its use in for loops. I'd like to know if Iterator can be constified under this proposal.
@Aloso Iterator should be possible with this RFC, however with how many methods it has that is quite difficult to do all at once.
AFAIK the current plan for constifying Iterator is to add a way to add a way to declare some trait methods as always non const with something like a rustc_non_const_trait_method attribute (see this issue). Not sure if that will be usable by non std code.
Deciding what exactly to constify in std is a Libs-API question and not really in scope for this PR.
The RFC template has under Alternatives, the question:
- What is the impact of not doing this?
Should this RFC have a section that reasons about the implications of NOT doing this?
For me on the sidelines, I see a complicated new syntax, but also don't know the big picture to where this takes us.
The design has pivoted to replacing ~const with (const) and no more impl const Trait for Type or const Trait, instead the presence of const or (const) methods makes that implicit. See also https://rust-lang.zulipchat.com/#narrow/channel/328082-t-lang.2Feffects/topic/.22constantly.20effective.22/near/503902077 for background on where this change came from.
As a random user, I think I prefer the alternative syntax of const meaning conditionally const, alongside =const meaning always const. It fits better with my intuition of what const means for functions, where the implementation and usage mean slightly different things.
- As an implementation,
impl const Trait for Tcurrently implies that any(const)methods may be markedconst, and so a theoreticalimpl =const Trait for Twould imply that all(const)methods must be markedconst. - As a bound these would imply the same thing;
constmeans the methods may be const, and=constmeans they must be const.
So in the example:
const fn default<T: const Default>() -> T {
T::default()
}
default would be usable in const contexts conditionally based on T's impl, but would always be usable in non-const contexts. Whereas in this version:
const fn default<T: =const Default>() -> T {
T::default()
}
I'd expect that default only exists for const impls of Default for T. The parity makes it more intuitive I think, since you use the more verbose syntax for the less common situation (I'm just guessing =const would be less common).
Please remember to create inline comments for discussions to keep this RFC manageable and discussion trees resolveable.
Note: the following issue is marked deprecated (and locked)
- https://github.com/rust-lang/rust/issues/67792
It says the tracking issue for the rewrite is:
- https://github.com/rust-lang/rust/issues/110395
but that obviously only tracks the removal of the old version.
I don't really know what the feature is supposed to be, but it sorta seems like this RFC should have a new tracking issue, and maybe the old one should link to the new one instead of to the removal tracking issue?
This RFC will get a new tracking issue once it gets accepted.
There are currently plenty of discussions happening in the background behind closed doors that have been influencing implementations (for example, rust-lang/rust#139858 mentions that the lang team decided on a new syntax but there is no link to where that discussion happened)
When @oli-obk said, over in https://github.com/rust-lang/rust/pull/139858, that,
Afaict the syntax with which to move forward was settled as much as it could be in the last lang team design meeting. The lang team wants to get hands-on experience with the feature to see how it feels, so this PR is ready to be merged now.
he was referring to:
- https://github.com/rust-lang/lang-team/issues/317
In that meeting, we didn't decide on the syntax. But, as Oli said, we need hands-on experience here, and along with the new proposal presented in that meeting and the lack of immediate strong objections, that's enough to move forward the experimental implementation work, as Oli did.
The doors aren't closed. Our meetings are open, and we keep extensive minutes that you can find linked from there.
Nevertheless, having proper tracking issues would be helpful. The current situation of reusing either 5-year old locked issues or 5-year-old closed issues as tracking issues is very confusing.
Agree of course we should properly track things. In my view, https://github.com/rust-lang/rust/issues/67792 is still the proper tracking issue for this work (as with many of our tracking issues, of course, it could use a lot of love to be brought up to date). I've gone ahead and unlocked it. I'd expect the reason we needed to lock it was temporary and hopefully the need for that has passed. If that's not the case, we can always lock it again.
The doors aren't closed. Our meetings are open, and we keep extensive minutes that you can find linked from there.
I should clarify, I wasn't saying that the information wasn't technically available, just that it wasn't immediately obvious where to look. For example, even among lang team meetings, it's not immediately clear which one it took place in, and the more time that passes between when a decision was made and when the decision is noticed by someone else, the harder it is to find which specific discussion occurred if it's not linked. It's entirely plausible that the discussion happened multiple meetings before the PR and it was only at that point where someone got around to implementing it.
This is kind of the reason why tracking issues exist: instead of linking every micro-discussion that happened elsewhere when it comes to important changes with respect to a thing, the unified tracking issue tracks any relevant decisions that happened. Precisely so you don't have to look through a bunch of irrelevant discussions in meeting notes, you can just have a bullet point in the tracking issue pointing out a PR that was made, which mentions in its description that it was after a discussion in a lang team meeting.
(Thank you for unlocking the issue, by the way; I'm mostly just adding a bit more context since I'm not 100% sure my point was clear.)
I don't know if this is open for discussion anymore, but I just wanted to say that I find the syntax using square brackets in [const] somewhat awkward. In other parts of Rust square brackets already mean the creation or indexing of some container, so using them here to indicate another thing alltogether feels like its overloading the meaning of square brackets too much.
I much preferred the old ~const syntax. It also had a pleasing symmetry with the other prefix modifiers like ?Trait and !Trait.
I was playing around with this today. And I noticed a potential inconsistency. In trait definitions, you have this theoretical syntax:
const unsafe trait Foo { ... }
But in implementation, you have this theoretical syntax:
unsafe impl const Foo ...
Perhaps it should be this?
const unsafe impl Foo ...
I suspect const is a property of the implementation not of the trait -- precisely the same as unsafe.
@npmccallum There is the precedent of unsafe const fn. Wouldn't it be more consistent to go with unsafe const impl then?
But then there's still the issue that the RFC proposes impl const Trait, so only unsafe impl const Trait would be consistent with that...
@npmccallum There is the precedent of
unsafe const fn. Wouldn't it be more consistent to go withunsafe const implthen?But then there's still the issue that the RFC proposes
impl const Trait, so onlyunsafe impl const Traitwould be consistent with that...
$ rustc --crate-type lib - <<EOF
const unsafe fn const_first() {}
unsafe const fn unsafe_first() {}
EOF
error: expected one of `extern` or `fn`, found keyword `const`
--> <anon>:2:8
|
2 | unsafe const fn unsafe_first() {}
| -------^^^^^
| | |
| | expected one of `extern` or `fn`
| help: `const` must come before `unsafe`: `const unsafe`
|
= note: keyword order for functions declaration is `pub`, `default`, `const`, `async`, `unsafe`, `extern`
error: aborting due to 1 previous error
Also, https://github.com/rust-lang/rust/pull/143879 puts const before unsafe.
To be clear, I don't care about the ordering of const vs unsafe. I'm only pointing out that unsafe trait requires unsafe impl but const trait requires impl const. That seems inconsistent to me.
In light of that, from what I can tell the syntax needs to be either:
-
const impl Trait/const unsafe impl Trait -
impl const Trait/impl const unsafe Trait -
impl const Trait/unsafe impl const Trait
to be self-consistent.
2 is impossible because the existing syntax is unsafe impl.
I suspect
constis a property of the implementation not of the trait -- precisely the same asunsafe.
const is part of the identification of the interface being implemented: “this impl block implements the const version of the trait”. unsafe is a modifier for the implementation itself: “I assert that this impl block fulfills the safety preconditions for implementing the trait”. It makes perfect sense, from that perspective, for unsafe to precede impl but const to precede the trait.
Regardless, I think it's worth mentioning that there should be parser recovery and proper linting to fix the keyword order here if people mess it up, like we have for ordinary functions.
https://github.com/rust-lang/rfcs/pull/3762#issuecomment-3124038598:
I don't know if this is open for discussion anymore, but I just wanted to say that I find the syntax using square brackets in
[const]somewhat awkward. In other parts of Rust square brackets already mean the creation or indexing of some container, so using them here to indicate another thing alltogether feels like its overloading the meaning of square brackets too much.I much preferred the old
~constsyntax. It also had a pleasing symmetry with the other prefix modifiers like?Traitand!Trait.
@oli-obk Any comment on this? Is the [const] syntax final?
No, but it is our best candidate at this time. We've had this bikeshed in 50 directions, it is not useful to restart it on this RFC. Please start a thread on the #T-lang/effects stream. But be aware that syntaxes have been discussed to death and there isn't much energy in T-lang or us compiler folk working on the feature for those discussions. While it sucks because of the distributed nature of all the previous discussions, I would really prefer if you read those first
No, but it is our best candidate at this time. We've had this bikeshed in 50 directions, it is not useful to restart it on this RFC. Please start a thread on the #T-lang/effects stream. But be aware that syntaxes have been discussed to death and there isn't much energy in T-lang or us compiler folk working on the feature for those discussions. While it sucks because of the distributed nature of all the previous discussions, I would really prefer if you read those first
I took a look at #T-lang/effects and could not find those discussions. I am sure they are somewhere in there, but the discoverability of stuff on Zulip is unfortunately not great.
It would be really nice if the RFC could include some overivew of these discussions and the current consensus of people working on this.
I believe that the [const] syntax was specifically decided in a lang team meeting, and as far as I'm aware, it's been substantially less discussed than the former ~const syntax. That doesn't mean it's necessarily better or worse, but I do think that a lot of people, myself included, have absolutely no idea what the justification was for it beyond "lang team wanted to try it out."
I get that these discussions are exhausting, and I don't think that syntax should necessarily block this RFC, but I do think that it's worthwhile collecting all those discussions in one place, rather than just asking people to scour Zulip for them. That is the purpose of an RFC, after all: explaining everything in one place so people don't have to piece everything together themselves from several fragmented discussion threads.
Nothing has been decided. My understanding is that there are some people on the lang team who like [const], but there is no consensus, and it hasn’t been discussed much in lang team meetings. The current nightly impl supports both, so people can experiment with whichever syntax they prefer.
I’m sympathetic to the concern of wanting discussions out in the open, but I don't think it makes sense to burden the GitHub discussion with endless conversation threads at this stage, while things remain so open-ended.
I should be clear: I don't think that we should copy all of the discussions that were had here or re-discuss them here, but I do think it would be nice if, at some point, we could collect them in a useful/actionable summary that could be added to the RFC. Honestly, it could even be added to the RFC after the fact, since like I said, I don't think this is something we need to block the RFC on when the semantics are the important part to nail down here. Even if we end up bickering later about the syntax, that should just be about the syntax, and not the semantics, which we should set in stone here.
Plus, GitHub constantly makes it difficult to have these discussions in the first place anyway, since it loves hiding every other comment on an issue thread. Overwhelmed by comments? Let's encourage more of them because people don't notice that something hasn't been discussed, because it was hidden, because we figured that you'd otherwise be overwhelmed.
Keeping things in their own review threads is an okay workaround, but it still doesn't do great for more all-encompassing discussions like these that don't really have a good spot in the diff to point to. That said, I'll make this my last comment on the matter for now. My main point was that I think it's vitally important to summarise these discussions as they happen because, well, it ends up frustrating for everyone involved if that doesn't happen: those who had the discussions have no energy left to summarise them because there were so many of them, and those who didn't partake in the discussions have no idea where to find them.