-
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.
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.
As someone who has spent much time helping people getting their hands on Rust, I'd like to second this.
I feel like having a Destruct
trait which is used only in this case but is different than Drop
(which you should not use in bounds anyway) is really confusing. This is not something that you can understand without knowing about rustc internals or the language history goes backward to the language great consistency.
Most people just want to know "can I drop this at compile time or not", they don't care if it is because of a manual Drop
impl or drop glue. Drop
is already a special trait, I think we can make it mean something else for ~const
bounds (and maybe change it later for normal bounds).
In short, I think that adding ~const Destruct
instead of ~const Drop
would make the language harder to understand.
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.
Most of the time you don't want to write out your impls by hand, but instead derive them as the implementation is obvious from your data structure. | ||
|
||
```rust | ||
#[const_derive(PartialEq, Eq)] |
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 looks like a typo - you talk below (consistently) about derive_const
, but this is const_derive
.
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.
Going to share this comment I made here: rust-lang/rust#90080 (comment)
Specifically, I'd like to point out that rust-lang/rust#67792 is a currently locked issue still being added as a tracking issue for several things depending on this RFC, with the caveat that "a new issue will be opened when the RFC is accepted."
We all know that this RFC is going to be accepted in some form or another, just, not sure what its final form will be. There are currently plenty of discussions happening in the background behind closed doors that have been influencing implementations (for example, rust-lang/rust#139858 mentions that the lang team decided on a new syntax but there is no link to where that discussion happened), and currently, they are all stuck on this old, locked tracking issue where nobody else is allowed to participate except those directly under the rust-lang org.
This should be resolved properly. Either, there needs to be a consensus that new stuff should not be done with the const traits (syntax, making traits const, etc.), or it should be acknowledged that things are being done under the assumption this RFC will be accepted in some form, and either the old tracking issues should be unlocked, or new ones should be made.
EDIT: I tried to follow your "only threadable discussions", "reminder" (it's not a reminder, it's a request, since this is not how the GitHub UI is designed to be used), but apparently leaving a review without linking it directly to a line in the source doesn't accomplish this. Sorry; feel free to create a new "thread," or just resolve this in a single comment so there's no need to have a thread.
When @oli-obk said, over in rust-lang/rust#139858, that,
he was referring to: In that meeting, we didn't decide on the syntax. But, as Oli said, we need hands-on experience here, and along with the new proposal presented in that meeting and the lack of immediate strong objections, that's enough to move forward the experimental implementation work, as Oli did. The doors aren't closed. Our meetings are open, and we keep extensive minutes that you can find linked from there. |
Nevertheless, having proper tracking issues would be helpful. The current situation of reusing either 5-year old locked issues or 5-year-old closed issues as tracking issues is very confusing. |
Agree of course we should properly track things. In my view, rust-lang/rust#67792 is still the proper tracking issue for this work (as with many of our tracking issues, of course, it could use a lot of love to be brought up to date). I've gone ahead and unlocked it. I'd expect the reason we needed to lock it was temporary and hopefully the need for that has passed. If that's not the case, we can always lock it again. |
I should clarify, I wasn't saying that the information wasn't technically available, just that it wasn't immediately obvious where to look. For example, even among lang team meetings, it's not immediately clear which one it took place in, and the more time that passes between when a decision was made and when the decision is noticed by someone else, the harder it is to find which specific discussion occurred if it's not linked. It's entirely plausible that the discussion happened multiple meetings before the PR and it was only at that point where someone got around to implementing it. This is kind of the reason why tracking issues exist: instead of linking every micro-discussion that happened elsewhere when it comes to important changes with respect to a thing, the unified tracking issue tracks any relevant decisions that happened. Precisely so you don't have to look through a bunch of irrelevant discussions in meeting notes, you can just have a bullet point in the tracking issue pointing out a PR that was made, which mentions in its description that it was after a discussion in a lang team meeting. (Thank you for unlocking the issue, by the way; I'm mostly just adding a bit more context since I'm not 100% sure my point was clear.) |
|
||
### Const trait bounds | ||
|
||
Any item that can have trait bounds can also have `const Trait` 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.
Is it allowed to write const Trait
or [const] Trait
bounds when Trait
is not declared #[const_trait]
/[const]
?
For example:
trait Foo {}
const fn bar<F>()
where
F: [const] Foo, // <-- Is this an error?
{}
I ask because currently the const_trait_impl
feature on nightly does require #[const_trait]
in order to write const
and [const]
bounds. So currently the example would fail to compile on nightly with something like:
error: `[const] can only be applied to `#[const_trait]` traits
I wasn't clear whether this is the intended behavior, or just an unstable feature being unstable. The quoted sentence here makes it sound to me like the bound should be allowed. But maybe it's ambiguous—it could also be interpreted as only defining where the const
/[const]
bound syntax is permitted, without precluding further restrictions. (Sorry if I missed a more relevant part of the RFC.)
I understand that const Trait
impls require Trait
to be declared [const]
, which helps ensure forward compatibility. But I don't think that concern would carry over to const Trait
and [const] Trait
bounds—they would just be trivially unsatisfiable if a const Trait
impl couldn't exist.
I think allowing this would be very useful to library authors, who could write [const] Trait
bounds on Trait
from external crates, while still supporting older versions of the crate where Trait
isn't declared [const]
yet. Adding a new const fn
in crate A isn't a breaking change; nor is adding [const]
/#[const_trait]
to Trait
in crate B (I think?). But if A had to bump the MSV of its dependency on B to add a new const fn
with a [const] Trait
bound, that would be a breaking change for crate A, which would make it more painful to start using the new feature gradually.
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 sentence applies to the item the trait bound is applied to, not the trait definition. The currently implemented behavior matches the RFC’s intent.
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 also simplify life for macro writers, in a similar way to allow_trivial_constraints
.
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 Foo {
fn foo<T: Bar>();
}
Cannot be implemented without the trait being marked, as otherwise you are making a decision for the trait author as to whether that bound is not const or conditionally const
There's lots of semver issues around that, I'm gonna add a section, but TLDR is "not possible for traits with generic methods, associated type bounds or super traits other than 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.
That's not what they were tasking about though? They were asking about bounds, not impls:
trait Foo { ... }
// Forbidden
impl const Foo for ... { ... }
// Could be allowed?
const fn bar<T: [const] Foo>(x: T) { ... }
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.
@chrisbouchard That’s a good example, thanks! I guess I have to amend my statement: [const]
should be forbidden in the where
clauses of these items. In your example, the [const] Input
bound is not in a where
clause, so it doesn’t break the property I listed earlier, and should be allowed. (I had never really thought about the fact that type Assoc: Supertrait;
and type Assoc where Self::Assoc: Supertrait;
mean different things. Would make a nice question for https://github.com/dtolnay/rust-quiz …)
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.
@Jules-Bertholet Hmmm. Again, I feel like [const] bounds in associated items' where clauses could be useful, but I haven't fully thought it through yet.
Could you express something like
[const] trait Trait<T> {
type Collection: [const] IntoIterator<Item=T>
where
<Self::Collection as IntoIterator>::IntoIter: [const] Iterator<Item=T>;
without a [const] bound in the where clause? (In a future where the iterator traits are [const].)
At least in this case, I think you could replace it with a second associated type
[const] trait Trait<T> {
type Iter: [const] Iterator<Item=T>;
type Collection: [const] IntoIterator<Item=T, IntoIter=Self::Iter>;
at the expense of a second associated type, which forces impls to spell out the iterator type.
(I suppose in this const iterator future IntoIterator::IntoIter
may already be [const] Iterator
, but not necessarily. E.g., you could construct an iterator at compile time that can only be iterated at runtime.)
I do feel like I see the outline of your concern, but I don't think I understand it fully. Could you share an example where a [const] bound in an item where clause makes const Trait
surprisingly weaker than Trait
? Thanks!
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 express something like
[const] trait Trait<T> { type Collection: [const] IntoIterator<Item=T> where <Self::Collection as IntoIterator>::IntoIter: [const] Iterator<Item=T>; }without a [const] bound in the where clause?
Yes. Just move the where
clause up:
[const] trait Trait<T>
where
<Self::Collection as IntoIterator>::IntoIter: [const] Iterator<Item=T>,
{
type Collection: [const] IntoIterator<Item=T>;
}
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 express something like
[const] trait Trait<T> { type Collection: [const] IntoIterator<Item=T> where <Self::Collection as IntoIterator>::IntoIter: [const] Iterator<Item=T>;without a [const] bound in the where clause?
or you could use the following, but it's not as general:
[const] trait Trait<T> {
type Collection: [const] IntoIterator<Item = T, IntoIter: [const] Iterator<Item = T>>;
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 place to look for what would become inexpressible here are associated functions (including e.g. RPITIT ones) with these [const]
WC bounds.
|
||
### Conditionally const traits methods | ||
|
||
Traits need to opt-in to allowing their impls to have const methods. Thus you need to mark the trait as `[const]` and all the methods will become const callable. |
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 took me a while to understand why this opt-in is needed, so here is a rewording suggestion that will make it more obvious:
Traits need to opt-in to allowing their impls to have const methods. Thus you need to mark the trait as `[const]` and all the methods will become const callable. | |
Making a trait's methods callable in const contexts imposes extra constraints on their default implementations (if any). | |
Therefore, traits need to opt-in to allowing their impls to have const methods. | |
You need to mark the trait as `[const]` and all the methods will become const callable. |
I don't know if this is open for discussion anymore, but I just wanted to say that I find the syntax using square brackets in I much preferred the old |
I was playing around with this today. And I noticed a potential inconsistency. In trait definitions, you have this theoretical syntax: const unsafe trait Foo { ... } But in implementation, you have this theoretical syntax: unsafe impl const Foo ... Perhaps it should be this? const unsafe impl Foo ... I suspect |
@npmccallum There is the precedent of But then there's still the issue that the RFC proposes |
$ rustc --crate-type lib - <<EOF
const unsafe fn const_first() {}
unsafe const fn unsafe_first() {}
EOF
error: expected one of `extern` or `fn`, found keyword `const`
--> <anon>:2:8
|
2 | unsafe const fn unsafe_first() {}
| -------^^^^^
| | |
| | expected one of `extern` or `fn`
| help: `const` must come before `unsafe`: `const unsafe`
|
= note: keyword order for functions declaration is `pub`, `default`, `const`, `async`, `unsafe`, `extern`
error: aborting due to 1 previous error |
Also, rust-lang/rust#143879 puts To be clear, I don't care about the ordering of |
In light of that, from what I can tell the syntax needs to be either:
to be self-consistent. 2 is impossible because the existing syntax is |
|
Regardless, I think it's worth mentioning that there should be parser recovery and proper linting to fix the keyword order here if people mess it up, like we have for ordinary functions. |
@oli-obk Any comment on this? Is the |
No, but it is our best candidate at this time. We've had this bikeshed in 50 directions, it is not useful to restart it on this RFC. Please start a thread on the #T-lang/effects stream. But be aware that syntaxes have been discussed to death and there isn't much energy in T-lang or us compiler folk working on the feature for those discussions. While it sucks because of the distributed nature of all the previous discussions, I would really prefer if you read those first |
I took a look at #T-lang/effects and could not find those discussions. I am sure they are somewhere in there, but the discoverability of stuff on Zulip is unfortunately not great. It would be really nice if the RFC could include some overivew of these discussions and the current consensus of people working on this. |
as they need to actually call the generic `FnOnce` argument or nested `PartialEq` impls. | ||
|
||
|
||
# Future possibilities |
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 believe that the I get that these discussions are exhausting, and I don't think that syntax should necessarily block this RFC, but I do think that it's worthwhile collecting all those discussions in one place, rather than just asking people to scour Zulip for them. That is the purpose of an RFC, after all: explaining everything in one place so people don't have to piece everything together themselves from several fragmented discussion threads. |
Nothing has been decided. My understanding is that there are some people on the lang team who like I’m sympathetic to the concern of wanting discussions out in the open, but I don't think it makes sense to burden the GitHub discussion with endless conversation threads at this stage, while things remain so open-ended. |
I should be clear: I don't think that we should copy all of the discussions that were had here or re-discuss them here, but I do think it would be nice if, at some point, we could collect them in a useful/actionable summary that could be added to the RFC. Honestly, it could even be added to the RFC after the fact, since like I said, I don't think this is something we need to block the RFC on when the semantics are the important part to nail down here. Even if we end up bickering later about the syntax, that should just be about the syntax, and not the semantics, which we should set in stone here. Plus, GitHub constantly makes it difficult to have these discussions in the first place anyway, since it loves hiding every other comment on an issue thread. Overwhelmed by comments? Let's encourage more of them because people don't notice that something hasn't been discussed, because it was hidden, because we figured that you'd otherwise be overwhelmed. Keeping things in their own review threads is an okay workaround, but it still doesn't do great for more all-encompassing discussions like these that don't really have a good spot in the diff to point to. That said, I'll make this my last comment on the matter for now. My main point was that I think it's vitally important to summarise these discussions as they happen because, well, it ends up frustrating for everyone involved if that doesn't happen: those who had the discussions have no energy left to summarise them because there were so many of them, and those who didn't partake in the discussions have no idea where to find them. |
FYI, I decided to throw together a crate to help increase testing of this feature: |
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. | ||
|
||
# Alternatives |
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 two important alternatives are missing from the alternatives section. While it is clear that
the team is set on the general direction, I think it's worth-while to at least mention them:
Alternative I, automatic const
We allow running any code at compile time, as long as it doesn't invoke
FFI/syscalls/target-specifific intrinsics, etc. Errors are moved from const fn
declarations to
calls in const
context.
Alternative II, optimistic const
Aka, the C++ approach. C++ also has const
(called constexpr
), but, as far am aware, they don't
have a second [const]
modality. I think this works becaues constexpr
means that a function
"may" be const-evalable for some values of the arguments, not that it "must" be const-evalable for
all arrguments. Here's the litmus test example of the semantics:
#include <iostream>
constexpr int f(bool b) {
if (b) std::cout << "See you at runtime!";
return 92;
}
int g(bool b) {
if (b) std::cout << "See you at runtime!";
return 92;
}
constexpr int a = f(true); // error
constexpr int b = f(false); // works (!!!)
constexpr int c = g(false); // error
I am a bit surpsised that these alternatives don't get at least some considiration. They of course
push some of the semver checking from compiler to the human, but the benefit of semver checking
constness seems much less important than for trait checking, and doesn't allow separate
type-checking (you still have to actually run consteval in the end of the day, and it still can
fail with division by zero or some such). In particular, C++ version does sound like it might be a sweet spot? It allows library authors to declare their intention, and to (conservatively) make functions as definitely-non-const, while avoiding the entire mechanism of const-conditionality.
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 reason Rust has not considered or rejected those alternatives is because it goes against the precedent we have in the trait system compared to C++'s template system where in Rust everything is checked ahead of time rather than leaving it for later like C++ where you have no idea if it works until you try and use it whereas Rust gives you an error immediately making it much more resilient against bugs.
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.
where in Rust everything is checked ahead of time rather than leaving it for later
This is not true for const generics/const fn.
fn foo<const N: u32>() -> u32 {
const { N - 1 }
}
fn main() {
foo::<1>();
foo::<0>(); // Doesn't compile.
}
More generally, given const X: u32 = some_function()
, you can’t really say whether this’ll work or not, you have to evaluate the function to check whether it terminates. const
annotations doesn’t really change anything about this.
It does seem to me that theres a very meaningful difference here in how traits and const fn s behave.
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.
CC [@]fee1-dead's very good reply to [@]matklad's general proposal which was first posted on lobste.rs: https://lobste.rs/s/jslvmu/const_trait_counterexamples#c_ixouyi
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 I fully get the semver point raised there. Is the point that adding std::cout << "See you at runtime!"
breaks semver? That's just like adding panic!
to a const fn though. Any change in behavior in a const fn could break compilation of a downstream crate. I could see that it is easier to realize that panic!
can break users than calling a function without realizing it is not const fn, but that seems like a quantitative difference to me, not a qualitative one. So I do agree that it is worth weighing this against the complexity we have to pile up to avoid such accidental non-const-fn calls.
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 we should try to avoid what happens in Python where you pass in some object and you only figure out that the object didn't support what you need when you run it and hit some corner case, or even worse, your library needs a particular interface but doesn't use every part of it, but a later version does use the rest of it, but now that breaks all your users who were just passing in objects that implemented only whatever the previous version of your library used.
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.
Indeed, though we can preserve some checking, and there's actually some flexibility in what we allow. I think the C++ semantics is this:
fn g() {}
constexpr fn f1() { g() } // declaration site error
constexpr fn f2(b: bool) {
if b { g() } // errors when f2(true) is called
}
that is, you get early errors if you are clearly calling something non-constexpr.
We can actually go further and make both of those into errors, and leave dynamic errors only for calls that can't be resolved statically (when you actually have T: Eq
type parameter, and not when you use concrete Foo::eq
).
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.
well, I would like it to also error when you have:
pub const fn f<T: PartialEq>(a: &T, b: &T) {
a == b
}
struct S;
impl PartialEq for S {
// not a const fn
fn eq(&self, other: &S) -> bool {
if std::env::var("LOG").is_some() {
eprintln!("S::eq");
}
true
}
}
// errors even though nothing calls g
pub const fn g() {
f(&S, &S);
}
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 a C++ person that was sent a link to this discussion, having to put constexpr
on everything is annoying. And imagining a definition checked C++ where you not only have to mark the function as constexpr
but also the constraints is even more annoying.
C++ has reached a point where essentially everything is constexpr
as long as it is defined in a header, doesn't access global state, and doesn't call C functions. You can (and should, if you're a library author) write a lot of C++ code that is constexpr
. As someone who primarily writes library code at work, I don't remember the last time I have written a non-constexpr non-template function.
If Rust compile-time execution follows the C++ trajectory, even more Rust code can be const
than C++ code constexpr
: Global variable access is unsafe
and thus discouraged, and you don't have header/source files, so that limitation doesn't exist either. I'd bet in 5-10 years, Rust library developers are as annoyed with mandatory const
annotations as C++ library developers are right now with the constexpr
annotations.
I'm not involved in Rust, but I'd recommend that you explore the Alternative Ⅱ or even Ⅰ. Yes, the accidental semver violations are scary, but that problems also exist for something else—access to OS APIs. I made this argument five years ago in the C++ world (https://www.foonathan.net/2020/10/constexpr-platform/), but I'd argue that long-term const
should be treated just like e.g. Windows-specific functions should be treated, as a platform a crate can or cannot target. You don't have a windows
annotation on functions that can be executed on Windows, and executing a function that isn't marked windows
on Windows is impossible, because what if a future version wants to use Linux specific APIs? A crate just documents that it works on Windows.
Long term, once almost all Rust code can be executed at compile-time, you want to treat it just like any other platform you can execute Rust code on. Having to put mandatory annotations on everything at that point just means that the keyword to introduce a function is no longer fn
but const fn
.
Anyways, just my unsolicited 2cts.
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 article, the idea is interesting and does deserves a mention in "Alternatives". But there may be cultural difference that Rust prefers doing precise constraint validation via type system instead of in human language (documentations) if possible. And I personally like the former because human language is imprecise, unverifiable, and far more complex than all RFC combined1.
You don't have a
windows
annotation on functions that can be executed on Windows, and executing a function that isn't markedwindows
on Windows is impossible, because what if a future version wants to use Linux specific APIs?
That's not exactly true in Rust. We do have cfg(windows)
module which exposes Windows-only APIs. They are not callable on non-Windows platforms and are guaranteed (by semver) never call a Linux-only-API in the future. Generic APIs are exposed in generic modules with looser constraints: users can expected them to be callable, working and NOT instantly panic where they can call them (returning Err(_)
is still a valid and working impl). In the consteval world, this "expectation" translates to "if there is a pub const fn
with some constraints, you can expect calling it under specified constraints should always work".
Yes, someone can still violate this expectation by panic!()
inside, but that serves an escape hatch for unwritable complex constraints, which we are constantly reducing. Otherwise, the library author is to blame, not the caller, similar to an non-unsafe fn causes UB when it's called weirdly in a safe context (is considered the library's fault).
Indeed, though we can preserve some checking, and there's actually some flexibility in what we allow. I think the C++ semantics is this:
fn g() {} constexpr fn f1() { g() } // declaration site error constexpr fn f2(b: bool) { if b { g() } // errors when f2(true) is called }
If we accept this and simply propagate (semantically, not type-wise) all implicit preconditions to the caller const fn
instead of enforcing them, just after some layers of calls, preconditions grow unmanageable2: no one, including the author, knows or is able to describe what preconditions does the outermost const fn
have. This is a nightmare for the caller3. Defining "calling me with inputs that makes me error is an error" is not helpful to anyone but a catchall disclaimer.
Yes it is possible today to do this via panic!
, but again, I still want to blame library author, and don't want to be blamed for "calling it wrong by simply following its signature". 😢
Footnotes
I guess it would have to be |
Please remember to create inline comments for discussions to keep this RFC manageable and discussion trees resolveable.
Rendered
Related:
impl const Trait for Ty
and[const]
(conditionally const) syntax (const_trait_impl
) rust#67792