-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Make trait methods callable in const contexts #3762
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
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. |
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
.
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.
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'd like to see #[derive]
'ing being discussed in this RFC, it's notably absent from it and shouldn't be an afterthought in my opinion.
Namely, there should be a canonical/conventional mechanism for deriving a "const trait impl" from/for an ADT.
Whether such an impl would be (unconditionally) const, conditionally const, or either depending on a "flag" would be a point for discussion.
It should be quite obvious why you'd want to const-derive a trait and why #[derive]
itself shouldn't derive "conditionally const trait impls" by default.
As for the concrete design, there should be a canonical/conventional way to signal to the (built-in or user-defined) derive macro that a "const" trait impl is requested.
As @petrochenkov noted in rust-lang/rust#118580 (comment), rust-lang/rust#118580 (comment) and rust-lang/rust#118580 (comment) there are three extensible candidates:
-
Leveraging helper attributes. At call sites, that might look like
#[derive(One, Two)] #[one(const), two(const)] …
I guess. At def sites, it would just look like
#[proc_macro_derive(One, attributes(one))] pub fn derive_one(_item: TokenStream) -> TokenStream { … }
That design wouldn't require any additional changes to the language.
-
Giving derive macros a second input stream. At call sites, that might look like
#[derive(One(const), Two(const))] …
At def sites:
#[proc_macro_derive(One)] pub fn derive_one(_item: TokenStream, _arg: TokenStream) -> TokenStream { … }
Note that the one-argument form would still exist for backward compatibility. This arity-based dispatch is very much possible for proc macros. See e.g., PR Provide a way for custom derives to know if they were invoked via
#[derive_const]
rust#118580. Of course, we could choose to do something different here but in any case I'd prefer if we didn't force a new edition for this. -
Passing another input stream via "global data" (which is what e.g.,
Span::call_site()
uses under the hood). At call sites, that might look like#[derive(One(const), Two(const))] …
At def sites:
#[proc_macro_derive(One)] pub fn derive_one(_item: TokenStream) -> TokenStream { let _arg/*: TokenStream */ = extra_input_stream(); … }
For context, the compiler currently features the placeholder syntax #[derive_const]
under the experimental feature derive_const
. Obviously, that syntax is quite awkward and non-extensible (to other potential effects or data in general we might want to pass to proc macros in the future).
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 the derive syntax should reflect the bound syntax: #[derive(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.
It should be quite obvious... why
#[derive]
itself shouldn't derive "conditionally const trait impls" by default.
Perhaps you could elaborate on this. I can think of some reasons, but I want to be sure I've thought of all the reasons.
I think the derive syntax should reflect the bound syntax:
#[derive(const Trait)]
.
For a related syntax discussion, see:
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 a related syntax discussion, see #3715
unsafe
is a property of the derivation operation there, while const
is a property of the resulting derived impl
here. The syntaxes for each should be different, to reflect that distinction.
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 should be quite obvious... why
#[derive]
itself shouldn't derive "conditionally const trait impls" by default.Perhaps you could elaborate on this. I can think of some reasons, but I want to sure I've thought of all the reasons.
Yes, of course. For the sake of argument, let's assume that this RFC establishes the convention for derive macros to generate const trait impls either always or just by default.
- It would create a medium-term divide in the ecosystem: The large corpus of derive macros written by users before the hypothetical stabilization of feature
const_trait_impl
would naturally continue to generate non-const trait impls until migrated (which takes time) contrary to the ones created after it. This could be perceived as confusing by newcomers and frustrating by seasoned Rust devs. - If not made an edition item, it would implicitly and practically irrevocably alter the public API of all library crates which expose types to which built-in derive macros (from
core
/std
) were applied as suddenly said crates would promise more (conditionally) const impls which they can't go back on without a major version update. - (Conditionally) const trait impls obviously come with more requirements therefore "less can be derived (by default)". Think less about type parameters and more about the types of priv/pub fields which — from a probability perspective — likely only impl the trait in question "non-const-ly".
- Assuming that there's no opt-out (unlikely, I admit), going forward there would no longer be a canonical / conventional way to derive non-const trait impls (which are convenient and obviously allow for greater leeway wrt. the evolvement of the implementation) forcing users to write those impls by hand (possibly error prone) or to create their own conventions and mechanisms.
- Assuming that there's an opt-out, then deriving would still no longer mirror manual impls wrt. constness since constness is notoriously opt-in. That would also mean finding new syntax that's unlike anything preexisting (think
#[derive(nonconst Trait)]
or similar).
Feel free to correct me on / contest any of these points, some of these might be a bit weak or even irrelevant. I threw this together quickly, so yeah.
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're sticking with the already implemented #[derive_const(Trait)]
syntax and leaving it to future RFCs after unsafe
derives are finalized to come up with something better
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.
derive_const
only works for built-in derive macros though, are you saying that we won't support user-defined const-derives in the foreseeable future (rust-lang/rust#118304 (blocked by design concerns or rather total lack of design))?
Co-authored-by: Josh Triplett <[email protected]>
* `impl const Trait` (in all positions). | ||
* These are not part of this RFC. |
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.
Does this not contradict later parts of the RFC that seem to explicitly allow const
in RPIT?
text/0000-const-trait-impls.md
Outdated
|
||
### Impls for conditionally const methods | ||
|
||
Methods that are declared as `(const)` on a trait can now be made `const` in an impl, if that impl is marked as `impl cosnt 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.
Methods that are declared as `(const)` on a trait can now be made `const` in an impl, if that impl is marked as `impl cosnt Trait`: | |
Methods that are declared as `(const)` on a trait can now be made `const` in an impl, if that impl is marked as `impl const Trait`: |
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 `cosnt fn` trait bounds (see https://github.com/rust-lang/rust/issues/83452 for where we got "reject all trait bounds" 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 implementation correctness argument is partially due to our history with `cosnt fn` trait bounds (see https://github.com/rust-lang/rust/issues/83452 for where we got "reject all trait bounds" 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. | |
The implementation correctness argument is partially due to our history with `const fn` trait bounds (see https://github.com/rust-lang/rust/issues/83452 for where we got "reject all trait bounds" 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. |
text/0000-const-trait-impls.md
Outdated
impl<T: Default> const Default for Box<T> { | ||
fn default() -> Self { Box::new(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.
This impl shouldn’t compile, right?
```rust | ||
#[const_derive(PartialEq, Eq)] | ||
struct MyStruct<T>(T); | ||
``` | ||
|
||
generates | ||
|
||
```rust | ||
impl<T: (const) PartialEq> const PartialEq for MyStruct<T> { | ||
(const) fn eq(&self, other: &Rhs) -> bool { | ||
self.0 == other.0 | ||
} | ||
} | ||
|
||
impl<T: (const) Eq> const Eq for MyStruct<T> {} | ||
``` | ||
|
||
For this RFC, we stick with `derive_const`, because it interacts with other ongoing bits of design work (e.g., RFC 3715) |
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_derive
or derive_const
?
## Why do traits methods need to be marked 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.
Missing body
``` | ||
|
||
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’s just a future possibility, so we can always figure out these details later, but I still don’t understand why this might not be possible to do. Is it just an implementation difficulty concern?
const 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.
Oli said:
The RFC requires
const trait Trait
andimpl const Trait
again after discussing it with niko.Rationale is
Pro: When you change this, you have a "one shot" chance to adjust the effects of methods in the trait.
Pro: More clear what your intent is, allows for better diagnostics; if you add a
(const)
to a method without modifying the trait, we error and have a chance to explain what's happening a bit, because that may indicate you have a confused mental model.Pro: Syntax mirrors usage in other places:
T: const Trait
const fn foo<T: (const) Default>
andconst trait Default
Con: Redundant. (But we could always not require it.)
Regarding the marking of the trait, I'm OK with explicit syntactic indication in the current edition. In fact, I somewhat prefer it for the reasons you mention. But I see those reasons (and in particular the number 1 and 2 "pros" that you mention) as transitional, and over time, using editions, I'd like to wash out this distinction, and that leads me to prefer using an attribute for this.
If we mark the impls with const Trait
, that makes a certain sense, as it does match the bound syntax, and the const
in that position means something analogous. In one place, it's setting (or asserting) the value of the associated effect, and in the other, it's bounding it. That makes sense.
On the trait, though, we'd be overloading const
to mean something rather different. Probably I'd frame what it's doing as "allowing const
/ (const)
bounds on the trait and allowing the setting or asserting of the associated effect on impls of the trait." For that one, I'd rather just have an attribute to this effect and later have an attribute that disallows those things that we'd employ when flipping the default over an 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.
I'll add it to future possibilities that we can remove the const
from the trait decl and turn it into a lint or sth if you didn't add any const 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.
I think there's a good semantic reason to mark the trait: it indicates the presence of an associated effect variable in the trait. non-const traits do not need an associated effect variable and thus ideally shouldn't have one to avoid mysterious issues due to a hidden degree of freedom.
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.
trait Tr {
fn f();
}
impl Tr for () {
const fn f() {}
}
fn test1<T: Tr>() {
// `f` can't be used as `const` in a generic context.
const { <T as Tr>::f() }; //~ ERROR
}
fn test2() {
// Since `f` is refined as `const` for `()`, though, we can use it
// as `const` when we know the concrete type.
const { <() as Tr>::f() }; //~ OK
}
If we ever accept this, which I think we should, then the most sensible way to explain that, I'd suggest, is still with associated effects and the "impls of associated types/effects are always refining" rule.
That's among the set of reasons why I don't think it's prudent for us to bifurcate the space of traits long-term.
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.
Here's the parallel, by the way, that I draw for why that should work. This works today:
trait Tr {
async fn f();
}
impl Tr for () {
fn f() -> impl Future<Output = ()> + Send {
core::future::ready(())
}
}
fn spawn<T: Send>(_: T) {}
fn test1<T: Tr>() {
// `f(..)` can't be used as `Send` in a generic context.
spawn(<T as Tr>::f()); //~ ERROR
}
fn test2() {
// Since `f(..)` is refined as `Send` for `()`, though, we can use
// it as `Send` when we know the concrete type.
spawn(<() as Tr>::f()); //~ OK
}
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.
Put simply, once we stabilize this, I'm planning to opt-in all of my traits and to require that in my projects for new ones.
What about traits for doing heap allocations, or I/O? For some traits, it doesn’t make sense to make them 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.
What about traits for doing heap allocations, or I/O? For some traits, it doesn’t make sense to make them
const
.
How do you know that the implementation is performing a system call?
For example, have a look at the Store API (#3446) which would aim at replacing the Allocator API. It was specifically designed to allow "in-line" memory, ie for [T; N]
to be used as the Store type and raw memory arena both, in which case even though it's an "allocation" API, no system call is required and it should be perfectly possible to call and return from a const context.
Similarly, even if the trait is FileSystem, there's no reason that a fake/mock implementation cannot be supplied in a const context.
As such, as long as a trait can be used in a const context even if some methods cannot -- for example because they return a raw file descriptor -- there's no reason not to allow a user to provide a const implementation, and allow them to use this const implementation in a const context.
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, there are cases where, with extra thought and design, you might be able to design a sensible const
version of the trait even in such domains. But there are also cases where you can’t (like your example of raw file descriptors, but also something as simple as “this returns a Vec
”). And even where it’s possible, the trait author might choose not to do so yet, as they may not be certain of the right API, or they may want to wait for upcoming const
features in the language or in their library dependencies.
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 it seems easier to just not allow refinement and avoid all this complexity.
People made this argument about refinement with RPITIT as well. We defended it on the basis that "RPITIT is just hidden associated types, and associated types are always refining." I don't believe that RPITIT would have been better without this kind of refinement. And having defended it with RPITIT, I'm probably especially skeptical that we should foreclose or plan to foreclose doing the same thing with hidden associated effects when there's a clear parallel.
Yes, until and unless we have more features there will be some limitations. We often choose to live with those for awhile while keeping doors open.
To the point of your example about blanket impls over wrapper types, there's no current way to express this either, even after we ship RTN:
trait Tr {
async fn f(&self);
}
impl<T: Tr<f(..): (Copy)>> Tr for MyBox<T> {
fn f(&self) -> impl Future<Output = ()> + (Copy) {
(**self).f()
}
}
And of course, until we ship RTN, even the version without conditional bounds isn't and hasn't been possible to express (without desugaring the trait away from AFIT/RPITIT, at which point you have to start writing manual Future
impls to implement the trait since we haven't shipped ATPIT either).
Also wrt the default being const. The same could be said for const fn in general. So maybe these should be considered together.
These are not comparable. Flipping the default on fn
means making all function bodies const contexts by default and then making runtime
an explicit effect somehow. Flipping the default on trait
doesn't mean anything like that, especially now that we're marking all items. It doesn't require us to (un-)invert an effect and all that entails.
One of my concerns about extending const
to this position is that it overloads const
in a different and unrelated way. The fact that even we're tempted to equivocate between these very different semantics in our own discussions just because of the shared keyword does not lessen my concern about this.
I think all this is out of scope of this RFC though
If we're going to require syntax to opt-in traits, we should settle that syntax in this RFC.
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 ever accept this, which I think we should, then the most sensible way to explain that, I'd suggest, is still with associated effects and the "impls of associated types/effects are always refining" rule.
I don't think so. The trait has no associated effect in this case. We just change how we typecheck trait method calls if we can already resolve to the impl, basically entirely bypassing the trait and treating this as if it was an inherent method call. I think we have to do that anyway, otherwise we can't explain e.g. unsafe
refinement or weaker trait bounds.
So, accepting that code makes no statement at all about the trait, since the trait is being bypassed in the relevant call. We can still meaningfully say that this trait simply does not have an associated effect. And my feeling is still that since an associated effect is a new degree of freedom with tangible consequences, we need some syntax to indicate its presence.
If I try to phrase your position in this framework, I think you'd say that the associated effect doesn't actually have any consequences, we cannot observe it doing anything unless there's a const impl
(i.e., unless there's an impl that sets the associated effect to anything non-default) and there you already agreed to having syntax? Hm... I'll have to think more about this.
text/0000-const-trait-impls.md
Outdated
impl const Default for () { | ||
const fn 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.
Oli said:
The RFC requires
const trait Trait
andimpl const Trait
again after discussing it with niko.Rationale is
Pro: When you change this, you have a "one shot" chance to adjust the effects of methods in the trait.
Pro: More clear what your intent is, allows for better diagnostics; if you add a
(const)
to a method without modifying the trait, we error and have a chance to explain what's happening a bit, because that may indicate you have a confused mental model.Pro: Syntax mirrors usage in other places:
T: const Trait
const fn foo<T: (const) Default>
andconst trait Default
Con: Redundant. (But we could always not require it.)
For the marking of the impl, I'm OK with impl const Trait
(if we decide we want to mark impls), as it matches the bound syntax and means an analogous thing. In one place, it's setting (or asserting) the value of the associated effect, and in the other, it's bounding it. That makes sense.
Unlike with marking the trait, I don't see this one as transitional.
On the question of whether we want to mark the impls, I'll say that not doing so feels approximately equivalent to me to how one has to consider all of the fields of a type to know what auto traits that type implements, and marking it feels similar to #[derive(Copy)]
, for all of the good and bad that each of those entail.
Another element to consider is that if we don't mark the impl, then conceivably we could allow people to constify their impls ahead of the trait being updated. Then, those impls would automatically qualify for the bound when the trait was marked for that. I don't know how important that is. I could imagine maybe people caring who want their thing to work with both newer and older versions of some upstream crate for a trait and want their own downstreams to be able to take advantage of the const
bound when using the new upstream crate version. Marking the impl means this ecosystem transition has to be serialized. Maybe or maybe not that matters, but it's something to think about.
If we were to see impl const Trait
as essentially just a static assertion on the associated effect, maybe that opens another door of syntactic possibilities 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.
If we were to see
impl const Trait
as essentially just a static assertion on the associated effect
I’ve argued in favor of this previously, but it doesn’t strictly need to be in the MVP. However, if we want to be forward-compatible with this, we need a special case now for const Trait
s with no non-defaulted (const) fn
s—they should probably be forbidden for now. (We don’t want people relying on certain types only implementing non-const
Trait
, and then a rustc upgrade makes them implement const Trait
also)
* super trait bounds | ||
* where bounds | ||
* associated type bounds | ||
* return position impl 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.
Can you give an example of RPIT/describe the semantics?
This comment was marked as duplicate.
This comment was marked as duplicate.
|
impl<T: (const) Default> const Default for Thing<T> { | ||
(const) fn default() -> Self { Self(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.
Over in rust-lang/rust#139858 (comment), there was an ask to start a thread here about this (const) fn
syntax on methods in the impl.
Requiring this (const) fn
syntax in cases like the one above, where the method's constness as implemented necessarily depends on the constness of a generic from the impl header -- and consequently on the associated effect in the desugaring -- is where @nikomatsakis and I had landed back at the start of March. We had discussed it both ways, but we liked this one better in our our discussion. Among other things, we liked how this emphasizes the connection to the associated effect, and this syntax allows for copying signatures from the trait def to the impl.
Niko mentioned this in his write-up here, and I elaborated on this earlier in this thread in #3762 (comment) and in #3762 (comment). Please see those comments, particularly the first, for more details.
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 I'd a copy paste error. I have never intentionally written this syntax
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 is just more syntax salt solely for the purpose of some language purity. There is no meaningful difference unless we stop marking impls which would be a mess
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 is just more syntax salt solely for the purpose of some language purity.
No, it is using syntax to convey a conceptual difference: is this function's constness tied to the constness of the trait (and therefore dependent on (const)
bounds in the impl header) or not?
At least, that's what I understood it to do.
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.
sure, but is this difference actually useful? We need to check whether const fn
refers to a (const) fn
in the trait or a const fn
in the trait anyway. This is just a random thing users will need to learn without actually caring. Only from a language purity this is nice, but practically this has neither an effect on the compiler (beyond being annoying to implement) nor on users' ability to understand the code
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 seems quite relevant to me to understand which bounds I have in context. A const fn
cannot use (const) Trait
bounds of the impl block, right?
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 it was declared as (const) fn
in the trait it can afaict. Or I still don't understand the TC/niko model so maybe someone else should write this RFC because I'm lost honestly and it seems bad to have someone who dislikes a model (the one allowing const
, (const)
and normal fns in one trait) being the one to write the RFC just to walk into mines like these.
see also my confusion in #3762 (comment)
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 one allowing const, (const) and normal fns in one trait
Yeah fair, on that one I am with you, I don't think we need that for the MVP -- though we should keep it in mind as a future possibility.
73e98c4
to
fdd11fb
Compare
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