-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Make trait methods callable in const contexts #3762
base: master
Are you sure you want to change the base?
Conversation
I love it and I’m very excited to get to replace the outlandish const fn + associated const 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. |
text/0000-const-trait-impls.md
Outdated
const fn foo<T: Trait<bikeshed#effect = ~const> + OtherTrait<bikeshed#effect = const>>(t: T) { ... } | ||
``` | ||
|
||
## Make all `const fn` arguments `~const Trait` by default and require an opt out `?const Trait` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this needs to be expanded on quite a bit more in the main text.
Up until reading this section, I actually hadn't really considered the examples below. I think it's really important describe better the different ways you might use a const (or maybe const) trait both inside and outside of const
contexts. The main text does a bit to motivate the need to distinguish between const and maybe-const bounds, but does not really go into why you need the distinguish between maybe-const and not-const bounds. The first example below hints a bit at it, but in my opinion is pretty incomplete. For my own sake, I expanded the first example below to make it work on nightly and to show how the code must change between going from with non-const bounds (https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=25b7e140187aca91a5ce377b42f86140) and without (https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=7ce4396310758e4595704e4228f45d69). Now, what this doesn't do, for me, is show why being able to write the non-const bound version is actually useful. A real-world example would go far here.
Now, going into this RFC, I was strongly in favor of this alternative (and still do favor it, though am slightly convinced by the associated const example below), for two reasons:
First, I expect the maybe-const bound to be what users want in the overwhelming majority of cases. In that sense, having "extra" noise to the syntax of bounds results in a reduced user experience. Though, as a counterpoint, the explicitness of them being "different" from what you see elsewhere is nice.
Second, ~const
bounds are new weird syntax. And, for many, Rust already has a lot of weird syntax, so we should be wary about adding more. On the contrary, users are already used to ?Sized
bounds, so ?const
bounds is not that weird or different. I am very unconvinced by argument below that this ~const syntax is "the one folks are used to for a few years now": 1) I expect plenty of people that will eventually use this don't actually use this on nightly 2) being "used to" a syntax does not automatically make it best.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Second,
~const
bounds are new weird syntax. And, for many, Rust already has a lot of weird syntax, so we should be wary about adding more. On the contrary, users are already used to?Sized
bounds, so?const
bounds is not that weird or different. I am very unconvinced by argument below that this ~const syntax is "the one folks are used to for a few years now": 1) I expect plenty of people that will eventually use this don't actually use this on nightly 2) being "used to" a syntax does not automatically make it best.
I was going to write similar will piggyback on this instead. ~const
reads to me very strongly as "not" or something along those lines, since ~
is bitwise not in C/++ and indicates a destructor in C++, and I think this is likely to be more understood by users than ~
meaning maybe. Not that we should necessarily base any syntax decisions off of C/++, but I don't know that a percentage of users being familiar with a nightly syntax makes that strong of a case either.
The ?
reads much stronger to me as "maybe", as in ?Sized
= "maybe sized" and ?const
= "maybe const".
This syntax was discussed recently at https://rust-lang.zulipchat.com/#narrow/channel/328082-t-lang.2Feffects/topic/Nadri's.20effect.20elision, and reasons came up for using ~
over ?
. However, reading again, I think the reasoning had the assumption that if ~const
were changed to ?const
then the exact semantics should be updated to nearly the same as ?Sized
. That is, changing the syntax would imply const trait
by default and ?const trait
opts out, similar to Sized
by default.
Imo the correlation doesn't need to go that deep though: we can say "read ?
as "maybe' ", or "?X
opts out of the default state of X
", i.e. use the ?
sigil with const
without changing any behavior here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, in an ideal world we'd do that, I mean, my original RFC from years ago proposed that. But that needs new syntax considering how many ppl are using nonconst bounds on const fns (and not just Copy
bounds).
You need those when you just need access to assoc consts or types, or when your struct has bounds, as you need to replicate those on the impls, even if the specific const fns you wrote don't need them.
The reason we gave up on ?const
was that we messed up the impl, because we made the impl try to mirror that. Today's impl is ~const
, opting into the constness, which is much simpler impl wise. We can invert the syntax, but the impl is opposite that and just how traits work. Yes traits have ?Sized
, but that's a thing we regularly get wrong somewhere in the impl, and we already know that adding new opt out traits is a breaking change, just like adding opt out constness would be.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Second,
~const
bounds are new weird syntax. And, for many, Rust already has a lot of weird syntax, so we should be wary about adding more. On the contrary, users are already used to?Sized
bounds, so?const
bounds is not that weird or different. I am very unconvinced by argument below that this ~const syntax is "the one folks are used to for a few years now":
I didn't say ~const
is what they are used to on nightly, but I see the ambiguity. What I meant was folks on stable are used to T: Trait
bounds existing and giving you only static access to the trait items.
I'll adjust the text
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I argued against using ?const
for implicit maybe-const, and opting out for non-const bounds semantics in my comment in the Zulip thread which was mentioned above.
~const
reads to me very strongly as "not" or something along those lines, since~
is bitwise not in C/++ and indicates a destructor in C++, and I think this is likely to be more understood by users than~
meaning maybe. Not that we should necessarily base any syntax decisions off of C/++, but I don't know that a percentage of users being familiar with a nightly syntax makes that strong of a case either.
I'd say that ?const
reads much more strongly as "not" for me, as with ?Sized
, so I don't like the idea of using ?const
as syntax of what ~const
does today. I mainly oppose this because of the dissonance implied in that: adding ?Sized
opts-out and relaxes requirements. Suggesting that ?const
(used in place of ~const
) would "opt-out" of traits being non-const by default is a stretch, especially since it doesn't relax requirements, it makes requirements stricter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, good point. When we see a fn
type anywhere outside a function signature we can't tell if this is ever meant to be used in const
contexts.
Maybe the defaults should be different for static and dynamic dispatch (the latter being fn()
and dyn Trait
types), but that could also easily be confusing. The RFC says next to nothing about the vision for dynamic dispatch so it's hard to compare.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(in reply to #3762 (comment))
context-dependent syntax
With your proposal, in a normal fn
, : Trait
just means “requires Trait
”, but in a const fn
, it means “requires Trait
in non-const context, and const Trait
in a const context”. The context of fn
vs const fn
affects the meaning of the construct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You already said that above, and I already answered it: The meaning of T: Trait
under my proposal is always "must be const in const context". Some functions just cannot be called in const context, making the requirement equivalent to "doesn't have to be const". I see no reason to explain this in such a complicated way as you did, and I disagree with the claim that this is context-dependent. We just have the simple rule that the bound, by default, matches the const context (i.e. behaves like ~const
) except when you want to change it by saying "must always be const" or "doesn't ever have to be const", which are the less common cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
less common cases
I don’t think it will be all that uncommon, because of marker traits with no const
version. And I think e.g. ?const Copy
is far more likely to cause confusion than anything in this RFC’s syntax, on top of being a strict downgrade from the current edition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The RFC says next to nothing about the vision for dynamic dispatch so it's hard to compare.
My personal thinking is that fn
pointers intended to be used in const contexts should be ~const fn
, and the same goes for dyn ~const Fn()
. That to me makes it much more consistent, as compared to the opt-out which might require a split. That is one of the reasons I support ~const
(or any syntax for opt-in) over ?const
(or any syntax for opt-out).
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as duplicate.
fe1331e
to
ff7fabe
Compare
Co-authored-by: Tim Neumann <[email protected]>
text/0000-const-trait-impls.md
Outdated
which we definitely do not support and have historically rejected over and over again. | ||
|
||
|
||
### `~const Destruct` trait |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this should just be ~const Drop
? Drop
bounds in their present form are completely useless, so repurposing them would make sense. Drop
would be implemented by every currently existing type, and ~const Drop
only by ones that can be dropped in const
contexts.
(Overall, very impressed by this RFC. It addresses essentially all the concerns I thought I might have going in. Thank you @oli-obk and team for all your hard work!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea that would be neat. But it needs an edition and giving the ppl that needed T: Drop
bounds the ability to still do whatever they were doing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the ppl that needed
T: Drop
bounds
Are there any such people at all? Making more types implement a trait should not be breaking or require an edition, no? Unless there is some useful property (for e.g. unsafe code) that only types that are currently Drop
have—and there isn’t, AFAICT. (Plus, removing an explicit Drop
impl from a type is usually not considered breaking.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still think it's very useful conceptually to split Destruct
and Drop
since the former is structural and the latter really isn't -- it's more like an "OnDrop
" handler. If we moved to ~const Drop
, then in order to write a well-formed ~const Drop
impl, you need to write where {all of my fields}: ~const Drop
in the where clause.
That is to say, there's a very good reason we split ~const Destruct
out of ~const Drop
in the first place :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's more like an "
OnDrop
" handler.
Yeah, that’s what it is right now, but we could expand its meaning.
in order to write a well-formed
~const Drop
impl, you need to writewhere {all of my fields}: ~const Drop
in the where clause.
The bound could always be made implicitly inferred. Drop
is extremely magic already, why not a little more?
But actually, I think it’s a good thing that these bounds can be specified explicitly, because it enables library authors to leave room for adding or changing private fields in the future. I could see allowing impl Drop
/impl const Drop
blocks with no fn drop()
method, that serve only to add restrictions on dropping in const
contexts. (In today’s Rust, you could use a ZST field for this.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would still be possible to change the meaning of
: Drop
over an edition
Right, so before we have a consensus on how that would look like, having both Destruct
and Drop
feels completely fine for me. Since we just want to know whether something can be dropped (T: ~const Destruct
) and we know that we will accommodate any existing uses of the Drop
bound and impls, any reformulating of how that works can still be done through an edition even if we choose to add Destruct
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing how random trait bounds of otherwise typical traits are presented, even over an edition, is not useful. It's not Fn, FnMut, FnOnce, or Sized. The mistake isn't that you can write a Drop bound, it's that Drop was handled by a typical trait, despite having atypical needs, and was not given special treatment to begin with. That is something you cannot simply change over an edition. Otherwise, introducing a magical special case too-late to help is not really for the best.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@workingjubilee Can you elaborate? To be clear, my suggestion is that Drop
should be like a normal trait (at least in terms of its trait bounds). The “magical special case” I suggested would be only for old editions, to preserve compatibility for the small number of people relying on the current not-like-a-normal-trait behavior (where a type that satisfies the Drop
bound is less capable than one that does not).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
??? Perhaps I misunderstood something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Current Drop
: trait bound satisfied only when an explicit impl
exists. Such an impl
must contain an fn drop()
. Adding such an impl
makes the type less capable.
Proposed Drop
: trait bound always satisfied on new editions (like this RFC’s Destruct
). Bare : Drop
bounds retain their current behavior on old editions (with a warning), for compatibility. ~const Drop
bounds behave like this RFC’s ~const Destruct
on all editions. Conceptually: when implementing Drop
manually, you override the default impl (like with an auto trait). An explicit impl
may specify ~const
bounds, or an fn drop()
handler. Adding such a handler implicitly (a) makes the type ineligible for destructuring, and (b) unimplements auto trait TrivialDrop
.
text/0000-const-trait-impls.md
Outdated
Since it's only necessary for a transition period while a crate wants to support both pre-const-trait Rust and | ||
newer Rust versions, this doesn't seem too bad. With a MSRV bump the proc macro usage can be removed again. | ||
|
||
## Can't have const methods and nonconst methods on the same trait |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How difficult would it be to support const
associated functions in traits from the get go?
trait Foo {
const fn ctfe();
fn runtime();
}
There's already significant support for const
functions today:
- Functions can be declared
const
, and invoked at compile-time. const
trait functions can be invoked at compile-time.
As an observer it seems a bit bizarre to me to launch const
traits without support for const
associated functions, which is likely to lead to churn in the ecosystem, rather than "close the gap" first.
But being just an observer, maybe I'm just misunderstanding how much work there would be to bring const
associated functions?
Note: I do understand we don't have them today, I merely think the RFC could perhaps take the stance they should be implemented before stabilizing const Trait
, it'll take a while anyway, so hopefully they would be!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As an observer it seems a bit bizarre to me to launch
const
traits without support forconst
associated functions, which is likely to lead to churn in the ecosystem, rather than "close the gap" first.
Which primary use case do you envision allowing const fn
in traits enable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As an example, consider the Store
proposal, which today implements the split-trait strategy in order to provide a const-constructible dangling handle, without forcing the user to have a fully const Store
, because calling OS primitives isn't const:
#[const_trait]
trait StoreHandle {
type Handle: Copy + ...;
fn dangling(&self) -> Self::Handle;
}
#[const_trait]
trait StoreSingle: StoreHandle {
fn allocate(&mut self, layout: Layout) -> Result<Self::Handle, AllocError>;
...
}
This is necessary for:
impl<T, S> Vec<T, S>
where
S: StoreSingle,
{
pub const fn new() -> Self
where
S: ~const StoreHandle
{
todo!()
}
}
The ideal interface would instead be:
const trait StoreSingle {
type Handle: Copy + ...;
// No reason NOT to have a const constructible handle type.
const fn dangling(&self) -> Self::Handle;
fn allocate(&mut self, layout: Layout) -> Result<Self::Handle, AllocError>;
}
Which makes for a simpler API, and enables a simpler implementation:
impl<T, S> Vec<T, S>
where
S: StoreSingle,
{
pub const fn new() -> Self {
todo!()
}
}
Is a simpler API sufficient motivation to wait for const associated function in traits? Or should the current trait API be split, knowing that splitting is a breaking change, and so is fusing them back when const associated functions make it to stable?
I am afraid that this is the kind of unfortunate trade-off that library maintainers will face, with users that don't care about the const asking to wait, and users who do care about const asking to make the two breaking changes.
It's an uncomfortable situation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do think this should be looked at, designed, and implemented. However I disagree with saying that we should prioritize this before const traits. Almost all traits in the standard library would benefit from having const traits, and the standard library currently does not have traits (or at least I don't know of anything) that requires all implementers to write a const fn
. It makes much more sense for us to try to get for x in 0..100 {}
in const working, and then see if this design pattern would be beneficial.
Also, it would be helpful if you could elaborate (perhaps with more context) on why an associated const wouldn't work in this case, though my point above still stands.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, it would be helpful if you could elaborate (perhaps with more context) on why an associated const wouldn't work in this case, though my point above still stands.
It's a good question, actually. I would expect in most cases S::DANGLING
would work quite well. The one tiny advantage of store.dangling()
is that the return value can be depend on the instance of store
. For example it may allow "randomization" by passing a different seed to store
, to help fuzz usecases where dangling handle is incorrect used to ultimately produce a reference. Quite niche, admittedly.
It makes much more sense for us to try to get
for x in 0..100 {}
in const working, and then see if this design pattern would be beneficial.
Oh I definitely wish to be able to use traits in const
contexts. And for
loops in particular.
Which is why I asked how much work it would be to support const
associated functions.
It seems like most of the scaffolding is here to me, but I am unfamiliar with compiler internals. If the answer is "it's a couple days work", then I'd argue it's really worth it to avoid all the potential churn (and maintainer pains) in the ecosystem. If the answer is "it's at least a month work, possibly a lot more as there are unresolved questions", then I'll back the decision to just stabilize const
traits first without reserve.
Would you have an educated guess as to the amount of work required?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And this is where one may rightfully ask why not use RTN instead of all of these individual patches around what RTN would just allow. Ignoring my opinions on RTN and the impl effort required for it, we'd then need to figure out what kind of problems that produces for API authors and users.
I have some objections to using RTN as a replacement for marking some trait methods as const:
- if we have a RTN-like syntax, it should not be
T::method(..): const
since that looks like marking the return type asconst
rather than the method itself, I think syntax likeT::method: const
orT::fn method: const
is better RTN-like syntax (there should be a better name than RTN since RTN is Return Type Notation -- syntax for getting the return type, not the method itself). - using RTN-like syntax makes trait bounds extremely verbose, e.g.
where T: MyTrait, T::foo: ~const, T::bar: ~const, T::baz: ~const,
To be clear, I'm not saying RTN-like syntax shouldn't exist, but that it should only be used where you can't just mark some trait methods as const
in the trait definition or mark the whole trait as const
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This potential for verbosity is shared with RTN also, and the answer we settled on in that case is that we would ship trait aliases, so that rather than repeating, e.g.,
where
T: MyTrait,
T::foo(..): Send,
T::bar(..): Send,
T::baz(..): Send,
...that either the trait author or a downstream user could write a trait alias such as trait SendMyTrait = ..
that states all those bounds once, and that in fact the trait author could use a proc macro (provided by our project) to wrap that up and never have to spell out those bounds at all, e.g.:
#[trait_variant::make(SendMyTrait: Send)]
trait MyTrait { .. }
In fact, that already works today without trait aliases, because we can polyfill that in other ways.
The point is, shipping some more precise and expressive mechanism doesn't mean that people are forced to verbosely repeat themselves. As with RTN, we can build on top of it to handle the common case.
In terms of a name, I'd probably boringly call this return effects notation (RKN). In terms of syntax, I don't think T::foo: ..
is right. That notates the type of the function item itself, which the set of output effects is not. If we had or planned a generic effects notation, e.g.:
fn f<effect K>(x: u8) -> u8 do K { x }
// ~~~~~~~~ ~~~~~ ~~~~
// Generic effect | |
// parameter Return type |
// |
// Output effects
Then I'd perhaps suggest to mirror that with RKN, directly extending RTN in a similar syntactic way, so we'd write, e.g.:
where T: Tr<foo(..): Send do const>
(There are other reasons that may make an RTN-style approach difficult in this case, but syntax or verbosity don't strike me as the blockers any more than they did for RTN.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree about the parallel to trait aliases, it motivates my proposal here.
output effects
What do you mean by “output” here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we were to write:
fn f<T: Send>() -> Vec<T> { Vec::new() }
// ~ ~~~~~~
// Input type Output type
We might say that the function takes an input type and produces an output (or return) type. Similarly, with generic effects, we could say:
fn f<effect K>() -> () do K - const { .. }
// ~ ~~~~~~~~~
// Input effect set Output effect set
That is, the function takes an input set of effects, and produces an output (or return) set of effects. The output set of effects are the effects that may be exhibited by the function itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"output effects" or "return effects" make no sense to me. In formal notation, the effects of a function are often annotated at the arrow, since they are part of the process from the inputs to the outputs. Also why do you abbreviate "effects" with "K" in "RKN"?
What you call "input effect set" and "output effect set" is an artifact of row polymorphism, I don't think it s a good guide for terminology here.
I'd call this something like "effect bounds" or so; it is about controlling the effects of trait functions.
text/0000-const-trait-impls.md
Outdated
A full example how how things would look then | ||
|
||
```rust | ||
const trait Foo: Bar + ?const Baz {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be an equivalent of this example with the current proposed syntax (const trait Foo: ~const Bar + Baz {}
), just to be clear and explicit about what supertrait bounds look like.
Ideally, there would also be an example with #![feature(trait_alias)]
(e.g. const trait Foo = ~const Bar + Baz;
); or alternately, those should be explicitly relegated to a future possibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall I am really excited about this; with &mut
out of the door, traits are the next big frontier for const. I fully agree we shouldn't block this on the async/try effects work; const is quite different since there's no monadic type in the language reifying the effect, and I also don't want to wait another 4 years before const fn can finally use basic language features such as traits.
My main concern is the amount of ~const
people will have to add everywhere. I'm not convinced it's such a bad idea to make that the default mode for const fn
. However that would clearly need an edition migration so it doesn't have to be part of the MVP. I just don't agree with the way the RFC dismisses this alternative.
{ | ||
... | ||
} | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the reference-level section, this seems more understandable than the <T as Default>::k#host = Conditionally
thing above, but maybe that's just because I have already through about the "be generic over constness" formulation quite a bit.
text/0000-const-trait-impls.md
Outdated
body to compile and thus requiring as little as possible from their callers, | ||
* ensuring our implementation is correct by default. | ||
|
||
The implementation correctness argument is partially due to our history with `?const` (see https://github.com/rust-lang/rust/issues/83452 for where we got it wrong and thus decided to stop using opt-out), and partially with our history with `?` bounds not being great either (https://github.com/rust-lang/rust/issues/135229, https://github.com/rust-lang/rust/pull/132209). An opt-in is much easier to make sound and keep sound. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first issue referenced here is not a ?const
issue, it is a min const fn issue, isn't it? We meant to reject const fn foo<T: Foo>()
but some loopholes were left open.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, which is my whole argument. An opt out is too easy to get wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The text says this is about the history of ?const
bounds, and then links to a PR that has nothing to do with ?const
bounds. That's at the very least confusing.
I also find this not a good justification for lang design decisions -- a borrowck is also easy to get wrong, so should we just not do it? Or a coherent trait system? IMO this is not a good argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the behavior is hard to understand for implementers, it’s going to be even harder for users. (Borrowck is a great example here!) Given that the simpler design can do anything the complicated one can, why bother with the latter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation concerns aren't about "hard to understand". The concept of "const fn should not have trait bounds" is trivial to understand, it just turns out it's easy to get wrong during implementation.
Given that the proposal differs from the RFC only in syntax, I don't think the proposed alternative is any harder or easier to understand than what the RFC says. The concepts people have to understand (and the concepts that have to be implemented in the compiler) are the exact same either way.
There are good arguments against making ~const Trait
the default. I think the RFC should focus on gathering those. I just don't think "we got something wrong in the compiler because it was implemented in a very syntax-directed manner" is a good argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a different argument from what the RFC makes, so it seems you are agreeing with me on the original point that started this subthread?
Not sure what you mean by "context-dependent syntax". But yes, an edition migration has downsides. Anyway please move discussion of the alternative proposal to the other thread; this thread is about the paragraph starting "The implementation correctness argument is partially due to our history". The argument that the RFC makes here is rather odd in two ways:
- The argument is not supported by the issue it links. I don't think "we got something entirely unrelated to
~const
wrong in the past" supports the claim that?const
is hard to implement. I don't buy the relation to?Sized
either, these are very different things --?Sized
adds a fundamentally new concept to the language that would not exist otherwise;?const
does not involve a new concept compared with~const
. - It's a weak argument to begin with, given that the entire underlying complexity here is about parsing. "We forgot to check
where
clauses" is a mistake that just sometimes happens; I don't buy the implicit claim that the proposed RFC is somehow immune to oversights like this (or less susceptible to them than the?const
alternative). The?Sized
issues arise because rustc architecture makes it somewhat tricky to support certain syntax only in a few specific places rather than everywhere. The obvious solution is to support the syntax everywhere. ;) More seriously though~const
apparently would also be supported only in a few places, so it has exactly the same issue.- If the syntax provides enough benefits, I don't think we should reject it for reasons like this. That would be really, really bad news for the future evolution of Rust. Maybe we should block it on a re-architecture of the parser that makes such issues less likely to occur, or so, but we can't stop adding good syntax just because our parser/lowering has a suboptimal architectures.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
~const
apparently would also be supported only in a few places, so it has exactly the same issue.
The linked things about other ?
bounds like ?Sized
or abitrary ?Trait
are not parser issues. They are handled "correctly" all the way to the type system, they are just either redundant there, because the traits are already opted out, or in the case of ?const
we were missing a requirement for an opt-out. It is much easier to forbid an opt-in, instead of requiring an opt-out, because you're forbidding the existance of something instead of forbidding the non-existance of something.
Anyway, I do not want to review, maintain or do that work (and thus be on the hook for any potential breakage around there), so 🤷 I will not talk about it anymore and others can figure it out. We can instead talk about the lang reasons not to do the opt out (other threads).
If the syntax provides enough benefits, I don't think we should reject it for reasons like this.
Agreed, but I consider this another nail in the coffin of ?const
, which I don't think we should have out of lang reasons
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm completely okay with the RFC comparing ~const
and ?const
entirely based on lang reasons and concluding that ~const
is the better choice for now. In fact I think that's exactly what the RFC should do -- the migration effort alone makes ?const
not suited for an MVP. But then the text that this thread is attached to (i.e., the implementation reasons) should be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know how to remove that without also removing the history. Feel free to propose a diff and I'll accept it. Otherwise I will leave it as is. Everything except these two sentences is about the non-impl things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still think "An opt-in is much easier to make sound and keep sound" is just not true (the difference is entirely syntax). And the issue linked here
due to our history with
?const
(see rust-lang/rust#83452 for where we got it wrong and thus decided to stop using opt-out)
is not about ?const
. So my proposed edit would be to just remove this paragraph. Since I assume you won't like that I'll add a proposal that just removes the IMO entirely unrelated points about ?Sized
.
text/0000-const-trait-impls.md
Outdated
Thus we give all `const trait`s a `~const Destruct` super trait to ensure users don't need to add `~const Destruct` bounds everywhere. | ||
We may offer an opt out of this behaviour in the future, if there are convincing real world use cases. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't that prevent const-ification of standard library traits? I.e. if we stabilize that (some) standard library traits imply ~const Destruct
, we won't be able to take that back.
It's also quite easy to come up with an example where we'd want a const
trait but not ~const Destruct
:
static EV: Vec<u8> = Vec::new(); // this works
static EV: Vec<u8> = Vec::default(); // so why shouldn't this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some good example traits that I don't think should ever imply ~const Destruct
: AsRef
, AsMut
, Borrow
, Deref
, PartialEq
, ToString
, fmt::Debug
(and other formatting traits, though fmt::Write
is probably an exception)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps the link here is that these traits permit ?Sized
types, whereas others don't?
Perhaps there could be an argument for making ~const Destruct
simply inherited in all the places with a default Sized
bound? This still doesn't answer the question of opting out, but at least it gives an idea of where it could be added by default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think ?Sized
is accidental here. The underlying thing is that if you do not accept T
by value, you don't need to destruct it.
It just so happens that if you don't accept T
by value, you also likely support !Sized
types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, a good example of where Sized
is required but Destruct
is not: the Hasher
in Hash::hash
(though if we could redo it it would allow ?Sized
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean, if you had const Hash
, wouldn't you also need the Hasher
to have ~const Destruct
here? I agree that the design isn't ideal, but given the current design, it seems to not be a good counterexample.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's also quite easy to come up with an example where we'd want a
const
trait but not~const Destruct
:
I think that's an ok limitation to have until we get a const heap.
I completely understand the concern, but the examples I came up with so far were all artificial or due to other limitations
Some good example traits that I don't think should ever imply
~const Destruct
:AsRef
,AsMut
,Borrow
,Deref
,PartialEq
,ToString
,fmt::Debug
(and other formatting traits, thoughfmt::Write
is probably an exception)
While I get that from looking just at the trait and a few of its usages, there are also not insignificant uses of those traits where you take the thing by value, not by reference, and the caller decides whether to give you a reference or the value itself.
I think if you are implementing const Deref
or const PartialEq
for your type, it's going to be rare that you actually don't want a ~const Destruct
bound.
Imo this relates closely to the discussions on linear types, and I'd rather take whatever solution comes up for that and reuse it for avoiding the need for the type to.
Wouldn't that prevent const-ification of standard library traits? I.e. if we stabilize that (some) standard library traits imply
~const Destruct
, we won't be able to take that back.
yea, that's actually what may be the thing that would prevent us implying ~const Destruct
. will need to think some more on this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps const Destruct
should be implemented for more types, with the help of const_eval_select
? For example, Vec
’s drop()
could be changed to support const
by panicking if a Vec
with non-zero capacity is dropped in const
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I proposed a possible solution in another thread, linking it here so it's easier to find: #3762 (comment)
text/0000-const-trait-impls.md
Outdated
which now need a `T: ~const Destruct` bound, too. | ||
In practice we have noticed that a large portion of APIs will have a `~const Destruct` bound. | ||
This bound has little value as an explicit bound that appears almost everywhere. | ||
Especially since it is a fairly straight forward assumption that a type that has const trait impls will also have a `const Drop` impl or only contain `const Destruct` types. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You do mention const Drop
impls later in the RFC with regards to bounds, but I'd make an additional note here or before here clarifying that const Drop
implementations are allowed, since at first glance it's not immediately clear what their purpose would be.
A simple example might be worthwhile: even though you can't mutate global state, you could for example have a value being borrowed inside the object which gets mutated in some way in the Drop
impl, which can be done in const context.
This also adds another interesting question: is forget
allowed for non-const Destruct
types? I would assume yes, but it's not clear here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also adds another interesting question: is
forget
allowed for non-const Destruct
types? I would assume yes, but it's not clear here.
It would be a breaking change to start disallowing it, so yes, it would continue to work
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually had no idea what the current behaviour was for forget, so, thank you for clarifying that. Definitely worth mentioning in the RFC itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see a good place to put this but I also found it fairly obvious that forget
is always fine for any input considering it has no bounds at all
text/0000-const-trait-impls.md
Outdated
Traits need to opt-in to being allowed to have const trait impls. Thus you need to declare your traits by prefixing the `trait` keyword with `const`: | ||
|
||
```rust | ||
const trait Trait {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing which is worth briefly touching upon is the idea of traits without methods being marked const
, since it technically does nothing by itself. It's a useful API guarantee if you choose to later add methods, but in the future, we may have traits which have an API guarantee to never have methods (e.g. marker_trait
), and it's not clear how we should handle those. Even though it's speculating about unstable features, I think it's worth at least mentioning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know what to write here. A trait without methods being const can be useful as a marker in blanket impls or just constraining what types can go into a function because you want to limit it without there being a type system reason in the body of the function (some unsafe stuff maybe?).
This comment was marked as resolved.
This comment was marked as resolved.
|
||
## Background | ||
|
||
This RFC requires familarity with "const contexts", so you may have to read [the relevant reference section](https://doc.rust-lang.org/reference/const_eval.html#const-context) first. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see constness as a binary property of an interface: either it’s const
or it’s not. You introduce new syntax (const Trait
, impl const Trait
, T: const Trait
and T: ~const Trait
), but is the initial problem (not being able to call a trait method (not marked const
) in a const-context an API problem? I’m not so certain.
My point is that if you have a function of the form const fn
, then it’s perfectly fine to call it at runtime. If you have to implement a fn
function, but provide its implementation with a const fn
, then it’s perfectly okay.
const fn default<T: Default>() -> T {
T::default()
}
Because the function is marked const
, we already have all the info in the type system to know that any method called with <T as Default>
must be implemented as a const fn
.
Then why not just accept that code, and only authorize calling it for T
implementing Default
by providing a const fn
for Default::default
? I.e. allowing this:
trait Default {
fn default();
}
impl Default for MyType {
const fn default() { MyType { x: 123 } }
}
I fail to see the unsoundness of doing this, because it should be allowed to implement a fn
with a const fn
, and I think that would solve most of the problems you try to solve?
EDIT: I realized I missed one situation you cover:
fn interior_const_default<T: Default>() {
const x = T::default();
}
This indeed would require to annotate something on T
to know that interior_const_default
will only work for T
implementing Default::default
with a const
. Hence, instead of annotating the trait, why not just adding that to a trait bound?
fn interior_const_default<T: Default<default: const>>() {
const x = T::default();
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please read the thread above discussing syntax, which has a list of reasons why deriving the constness of trait bounds from the context would not work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I read it, and I’m not entirely sure why (I might have missed an argument against it?).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't work because you can have trait bounds on const fn
now. You simply can't call any methods on the trait. So requiring all methods be const is a breaking change. And Rust (almost) never looks at the body of a function to see if a call-site is well typed. That would be a backwards compatibility hazard, since any change to the body of a function could be a breaking change.
Irrelevant note: if you are using impl trait in return position, then Send and Sync are inferred from the body, but this is the exception to the rule.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't work because you can have trait bounds on const fn now. You simply can't call any methods on the trait.
But that’s my point! Since we can’t call them, why not simply not just add constness bounds?
// current Rust
const fn default<T: Default>() -> T {
// we can’t call anything from T because Default::default is not const
}
// what would work
const fn default<T: Default<default: const>>() -> T {
// now we can cherry-pick what is const, and thus here we can
// call default::<T>() where <T as Default>::default() is const
}
What am I missing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What you originally proposed (allowing
const fn a<T: Foo>() { T::foo(); }
to just work) has been debated many times, and I posted some of the reasons in my linked thread. Besides the countless language design concerns needed to be considered for those semantics, I do not think the main stakeholders (project-const-traits) on the compiler side is willing to implement them.
Okay, I get that, hence why I suggested the syntax T: Trait<method: const>
.
For the Iterator
example, it would require Self::next: const
at the implementor level, right? Not at the trait definition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the Iterator example, it would require Self::next: const at the implementor level, right? Not at the trait definition.
How do you handle methods defined on the trait itself? Of which Iterator has many. Such methods would need to be const if and only if the next method of the implementation is const.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Default bodies are really kind of just bodies of blanket default impl
items in an RFC 1210 sense, so in that way, aside from how we might syntactically expose that for default bodies, it's perhaps the same underlying question of whether this could be handled at the impl level.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the Iterator example, it would require Self::next: const at the implementor level, right? Not at the trait definition.
How do you handle methods defined on the trait itself? Of which Iterator has many. Such methods would need to be const if and only if the next method of the implementation is const.
My thinking here is that a where Self::method: ~const
bound on a const fn
in the trait definition would be considered to apply to the default implementation. For example, all the Iterator
combinator methods would have where Self::next: ~const
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you have to implement a fn function, but provide its implementation with a const fn, then it’s perfectly okay.
What if I also have
const fn default2<T: Default>(x: T) -> T {
x
}
This may seem like a silly function to write, but in general it may certainly happen that you write a const fn
with a bound and then don't use the bound to call a method. Maybe you just convert some &T
into &dyn Trait
, or you need an associated type or associated const of the trait but no method. (In fact you can already do that on stable.)
Would you say that calling that function with a T
whose Default
impl is non-const should be accepted or not?
- If it should be accepted, then your proposal violates the principle that the function signature fully determines how the type-checker treats this function.
- If it should not be accepted, that's a breaking change.
Such an ecosystem would also make `const fn` obsolete, as every `const fn` can in theory be represented | ||
as a trait, it would just be very different to use from normal rust code and not really allow nice abstractions to be built. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const
generics has limitations, so no, it would not make const fn
obsolete…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Redundant" may be the better word. And yes, we'd need more const generics features
text/0000-const-trait-impls.md
Outdated
Arguments and the return type of such functions and bounds follow the same rules as | ||
their non-const equivalents, so you may have to add `~const` bounds to other generic | ||
parameters, too: | ||
|
||
|
||
```rust | ||
const fn foo<T: ~const Debug, F: ~const Fn(T)>(f: F, arg: T) { | ||
f(arg) | ||
} | ||
|
||
const fn bar<T: ~const Debug>(arg: T) { | ||
foo(baz, arg) | ||
} | ||
|
||
const fn baz<T: ~const Debug>() {} | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was interested in the question of in what cases the ~const
in foo<T: ~const Debug, ..>
might be needed. Here's a mockup to check my understanding:
That is, I gather that it's not needed, which is I suppose not surprising, though a bit subtle, since the Debug
bound isn't needed there either.
If that encoding is wrong, of course, let me know; otherwise perhaps it'd be more clear to write this example as const fn foo<T, F: ~const Fn(T)>
.
text/0000-const-trait-impls.md
Outdated
While that can in generic contexts always be handled by adding more `~const Destruct` bounds, it would be more similar to how normal `dyn` safety | ||
works if there were implicit `~const Destruct` bounds for (most?) `~const Trait` bounds. | ||
|
||
Thus we give all `const trait`s a `~const Destruct` super trait to ensure users don't need to add `~const Destruct` bounds everywhere. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In working through some examples, I find that the other place that ~const Destruct
bounds are needed rather pervasively is on associated types. E.g. (simplified):
const trait FnOnce<Arg> {
type Output: ~const Destruct;
fn call_once(self, arg: Arg) -> Self::Output;
}
The idea, I gather, is to leave these to be written explicitly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a possible solution to this and to the issue that many traits don't need a ~const Destruct
super trait is to operate the same way that having a type &'a T
implicitly adds a T: 'a
bound: automatically add ~const Destruct
bounds on all types that are function argument/return types. so, e.g.:
pub const trait AsRef<T: ?Sized> { // no implicit `~const Destruct` bound
// `&self` is an argument, so add `for<'a> &'a Self: ~const Destruct` -- which does nothing
// `&T` is a return type, so add `for<'a> &'a T: ~const Destruct` -- which does nothing
fn as_ref(&self) -> &T;
}
pub const trait Default {
fn default() -> Self; // `Self` is a function's return type, so add `Self: ~const Destruct`
}
pub const trait FnOnce<Arg> {
type Output;
// `self` is an argument, so add `Self: ~const Destruct`
// `arg` is an argument, so add `Arg: ~const Destruct`
// `Self::Output` is a return type, so add `Self::Output: ~const Destruct`.
fn call_once(self, arg: Arg) -> Self::Output;
}
I'm not sure if those bounds should be added to the trait, or merely to the trait's methods, or some combination of that. There should probably be a way to opt-out of adding implicit bounds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dyn Trait
could also have ~const Destruct
bounds deduced, inspired by how dyn Trait
lifetime deduction works:
&dyn Trait
and &mut dyn Trait
both default to not having ~const Destruct
bounds (similar to how they default to having the same lifetime as the reference), other dyn Trait
default to having ~const Destruct
bounds (similar to how it defaults to being 'static
).
The design has pivoted to replacing |
|
||
It is now allowed to prefix a trait name in an impl block with `const`, marking that this `impl`'s type is now allowed to | ||
have methods of this `impl`'s trait to be called in const contexts (if all where bounds hold, like usual, but more on this later). | ||
Traits can declare methods as `const`. Doing so is a breaking change, as all impls are now required to provide a `const` method, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
adding const to an existing method is a breaking change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing it also breaking, no? As consumers can no longer rely on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the point is adding a new defaulted function is fine to be const
. it's when you add const
to an existing function
|
||
### Conditionally const traits methods | ||
|
||
Traits need to opt-in to allowing their impls to have const methods. Thus you need to prefix the methods you want to be const callable with `(const)`: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think (const)
is a step-down from ~const
.
type Foo = (i32);
is equivalent to type Foo = i32;
and let x = (7)
is the same as let x = 7
—there is a prior expectation that freestanding parentheses are just for grouping and don’t change the meaning of the contained construct. (const)
breaks this expectation, which is misleading and confusing for users.
In contrast, ~
has a natural connotation of “maybe” or “sort of”, which is quite close to the concept being conveyed. It’s a natural fit. In my view, users having to learn a new sigil is preferable to users having to learn a confusing new overloaded meaning for an existing sigil.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quoting myself
I'm gonna die on the hill that I support whatever gets landed faster
It's just painting the shed now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also: parentheses-around-const
has another potential meaning.
Consider this function:
const fn foo<T: Bar>(x: T) -> impl Baz {
/* ... */
}
Imagine we wanted to specify that the return type should be const Baz
if and only if T: const Bar
. One potential syntax would be:
const fn foo<T: Bar>(x: T) -> impl (const where T: const Bar) Baz {
/* ... */
}
It would be a shame to paint ourselves into a corner here!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Under this RFC it'd be:
const fn f<T: (const) A>(x: T) -> impl (const) B { x }
This works in nightly (under the old syntax).
For me, this argues in the opposite direction of your point. We could see the above as equivalent to (something like) this, using your syntax:
const fn f<T: A>(x: T) -> impl (const where T: const A) B { x }
And we could say that we're treating that bound as the default in the presence of the earlier T: (const) A
bound and are eliding it. That is, the parentheses seem actually more scalable in this way, as if we did later want to extend them with some kind of more explicit syntax like this, it seems we could probably do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In contrast,
~
has a natural connotation of “maybe” or “sort of”...
For me, depending on context, ~
either means "not" or "approximately", and I think of "approximately" and "maybe" (in the sense meant here) as being rather different. (We do use the word "maybe" to mean "approximately" sometimes, informally, e.g. in "it happened maybe ten thousand years ago", but that's not the sense of the word "maybe" that we're interested in.)
If we had nothing else, of the limited options available on the keyboard, then maybe we could lean into that pun, but it was never a perfect fit, in my view.
The intuition for parentheses is that we say things like "as always, the software will definitely (not) ship on time," or "if we see an increase (decrease) in the metric, we'll relax (strengthen) our spending controls". That is, we use the parens when we mean something to be read in either of two ways. That fits pretty well with what we're trying to express here.
(As we can see in that second example there, we sometimes tie the parentheses together, such that they're all meant to be toggled as a unit, which is something else that we're doing here.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don’t know if my hypothetical (const where ...)
syntax is a good idea. Though I believe we will likely need some way to express these nuances, but I have no strong opinions about what that should be. The only reason I brought it up was to point out the danger of (const)
restricting our future options by using up the most common syntactic grouping construct (parentheses) unnecessarily.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But at that point, to achieve that level of control (and more), I'd prefer to just spell the generic effect parameter explicitly anyway, e.g.:
const fn f<effect K: (), T: A do K>(x: T) -> impl (B do K) { x }This reads, "the argument to the generic effect parameter
K
must be a subset of the default set of effects, which is spelled()
. The default set today is(runtime, panic, etc.)
. The associated effect<impl B as B>::Effect
on the output opaque will have the same set of effects as the associated effect<T as A>::Effect
on the input.
OK, but that’s still not the same thing! In my example, I don’t want to commit to anything regarding effects like panic
—only const
/the runtime effect. The traits in question aren’t necessarily even parameterized by those other effects!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I know... I omitted this for simplicity, and because generally I think people would just want to pass along all the effects. But we could write a WC bound to express what you want exactly.
(Fortunately we don't need to design all this today.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally I think people would just want to pass along all the effects.
I don’t. Again, the different traits might not even share the same set of effect parameters. And if this function is performing some sort of wrapping/unwrapping operation, then the wrapper might be using effects of its own.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I too would like us to have a way to support all such cases.
Note that this has two caveats that are actually the same: | ||
|
||
* you cannot mark more methods as `(const)` later, | ||
* you must decide whether to make a new method `(const)` or not when adding a new method with a default body. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add an example of a trait with a (const)
/~const
method, with a default body that calls other trait methods?
```rust | ||
trait Trait { | ||
(const) fn thing(); | ||
} | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I appreciate most of the changes made to where const
/(const)
/~const
annotations need to be placed, as they parallel my proposal at #3762 (comment). However, I think requiring the trait definition to say const trait Trait { /* .. */ }
was valuable as documentation. One should not have to scroll through the entire list of methods in order to determine whether a trait can be const
-qualified. Even though the compiler could infer this, I think users should have to write it out anyway.
(It may also be valuable to allow impl const Trait
as an assertion that the impl is const
, even if the const
keyword is no longer required in that position.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one other issue with switching to only-methods-get-const is afaict it's incompatible with const Sized
as proposed in #3729 since Sized
has no methods, but that is a meaningful distinction since size_of
's const
-ness depends on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@programmerjake I think the way to resolve that is that size_of()
should be made an associated function of Sized
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this is a silly idea, but couldn't size_of() instead be an associated const of Sized? Size is constant, it doesn't change at run time, as far as I am aware. (Adding this and then deprecating size_of() wouldn't be a breaking change, which is an added bonus.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think having an associated const would work for the use case of #3729 because it introduces the concept of types that whose size is not known at compile time (but have a consistent size for one execution), so their size can't be put in a const
at compile time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we were starting today, there'd be no reason for us to bifurcate the space of traits into const ones and not-const ones, just like how we didn't bifurcate traits into two spaces when we started allowing async items in traits.
This analogy is flawed. I’m not proposing that traits with only always-const
methods should be marked const trait
, just as traits with always-async
methods are not marked async trait
. Only traits with maybe-const
methods, such that T: const Trait
bounds can be written, should require the annotation. In this case, the bifurcation already exists: either you can write that bound, or you can’t. Requiring const trait
simply ensures that one can determine which category a trait belongs to in O(1) time from the definition, instead of O(n).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My mental model is that const Trait
is syntax sugar for a trait alias. It’s approximately equivalent to
trait ConstTrait = Trait
where
Self::method: const,
Self::other_method: const;
The const
in const trait Trait
syntax declares this alias.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes... you know, I knew someone would quibble with that first sentence by itself. The second one and the second paragraph (and the footnote) were intended to address that.
What you're trying to clarify there is actually exactly why, in my view, marking the trait with e.g. const trait
would increase confusion. It's because we only need to flag the trait (for transitional reasons) in the "maybe" case and not in the "always" case. So const trait
doesn't actually mean "this trait has const
methods", as a non-const trait might have all-const methods. It instead means "this trait has associated effect parameters for its items and supports bounds tied to those". And that's what we wouldn't distinguish if we were starting today -- the assumption would just be that all traits may have implicit associated effect parameters.
Further, the analogy given is better that it appears at first glance. AFIT and RPITIT add implicit associated types to the traits, just like this adds implicit associated effects, and the consequences of this due to auto trait leakage (i.e. the Send
bound problem) have a lot of overlap with the design problems here. E.g., we talked about breaking up the space of traits into Send
traits and not-Send
and maybe-Send
traits and other such things (but did not do that, except via a proc macro).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for transitional reasons
I disagree with this. I think that, even were this feature to have been introduced prior to 1.0, it would still be valuable to distinguish between traits that can be const
-parameterized and ones that can’t. For many traits, having two variants makes no sense. And even when it does make sense, it’s good to not force trait authors to commit until they are ready.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For many traits, having two variants makes no sense
Not to pick too much on one sentence, as I'm sure you understand the underlying model, but I want to highlight it, as this specifically (two variants of a trait) is exactly what we're not doing. And correspondingly, what would concern me about saying const trait
is that it would suggest to people that is what's happening, when it's not.
To demonstrate what I mean, this (Tr
) is a trait with two variants:
pub trait Tr<T: Tag> {}
pub trait Tag {}
impl Tag for u8 {}
impl Tag for u16 {}
That is, Tr<u8>
and Tr<u16>
are two variants of the Tr<T>
trait. Conversely, this is a trait without any variants:
pub trait Tr {
type Ty: Tag;
}
Here, there is "only one trait". I.e., the trait itself is not generic; there cannot be any variants of it, and correspondingly any single type can implement this trait only once (i.e. rather than once for each variant).
What we're doing, importantly, in this RFC is the effect equivalent of this second thing, not of the first.
If we were doing the first thing, then I'd agree that const trait
or similar would make a lot of sense.
trait Default { | ||
(const) fn default() -> Self; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's important to highlight somewhere that the intended intuition behind the parentheses in this position is that they explicitly represent what is tied to the associated effect. Following from this, @nikomatsakis and I mean to allow this as well:
impl Default for () {
() fn default() {}
}
And for that matter:
trait Default {
() fn default() -> Self;
}
The ()
says explicitly that the associated effect gets set to the default (e.g. runtime
, panic
, etc.), so these examples have the same semantics as if the ()
weren't there.
We're not suggesting that we're necessarily going to encourage this, simply that we want to allow it for consistency and pedagogy. When teaching people, using this form is part of our plan for helping people to really grok what's going on.
So anyway, let's mention somewhere in the RFC that this syntax is allowed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could lint against it, but disable the lint in macros. Then ppl can write macros that pick const or not easily
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point about it being good for macros.
### Sites where `(const) Trait` bounds can be used | ||
|
||
* `const fn` | ||
* `(const) fn` | ||
* trait impls of traits with `(const)` methods | ||
* NOT in inherent impls, the individual `const fn` need to be annotated instead | ||
* `trait` declarations with `(const)` methods | ||
* super trait bounds | ||
* where bounds | ||
* associated type bounds |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably we should mention RPIT and APIT specifically here if we mean to allow it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
APIT is just syntax sugar ™️
I'll mention RPIT
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, agreed, RPIT was the main thrust there.
* Allow marking `trait` impls as `const`. | ||
* Allow marking trait bounds as `const` to make methods of them callable in const contexts. | ||
|
||
Fully contained example ([Playground of currently working example](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=2ab8d572c63bcf116b93c632705ddc1b)): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we mean to allow this with RPIT, which I'd suspect we do, we probably want to add an example here also.
``` | ||
|
||
and use it in non-generic code. | ||
It is not clear this is doable soundly for generic methods. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not clear this is doable soundly for generic methods. | |
It is not clear this is doable soundly for generic methods. | |
## Macro matcher | |
In the future, we may want to provide a macro matcher for this optional component of a function declaration or trait declaration, similar to `:vis` for an optional visibility. This would allow macros to match it conveniently, and may encourage forwards compatibility with future things in the same category. However, we should not add such a matcher right away, until we have a clearer picture of what else we may add to the same category. |
Please remember to create inline comments for discussions to keep this RFC manageable and discussion trees resolveable.
Rendered
Tracking:
impl const Trait for Ty
and~const
(tilde const) syntax (const_trait_impl
) rust#67792