rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

Make trait methods callable in const contexts

Open oli-obk opened this issue 1 year ago • 32 comments

Please remember to create inline comments for discussions to keep this RFC manageable and discussion trees resolveable.

Rendered

Related:

  • https://github.com/rust-lang/rust/issues/67792

oli-obk avatar Jan 13 '25 16:01 oli-obk

I love it and I’m very excited to get to replace the outlandish const fn + associated const ~~hacks~~workarounds I’ve joyfully come up over the last years :D

Thank you for all of the hard work that everyone working on the various implementation prototypes and around has put into const traits!

juntyr avatar Jan 13 '25 19:01 juntyr

I didn’t see const implementations in alternatives. Is there a reason they can’t be considered ?

Eg. const impl PartialEq for i32 { … } where only this impl is const, no changes to the trait.

letheed avatar Jan 13 '25 22:01 letheed

In this proposal const fn foo<T: ~const Bar>() is a function that is const when Bar is const impl'ed for T. This doesn't feel very consistent to me, as const fn implies to me that the function is always const. Likewise, I don't like that const trait Bar { ... } is a trait that may be const impl'ed, but isn't required to be const. Here's what I propose as a way to make this all feel more consistent (in my opinion!)

  1. const means something must be const under all conditions.
  2. ~const means something will be const under certain conditions.
  3. Without const or ~const, a signature implies non-const, while a bound implies indifference to const.

Below is my justification for this belief. The tl;dr is I believe the syntax would be more consistent, and allow for sensible implied clauses without sacrificing API clarity.

// Function is const under all circumstances.
// This implies T: const Bar and S: const Baz
const fn foo<T: Bar, S: Baz>() { ... }

// Function is const if all trait implementations are const.
// This implies T: ~const Bar and S: ~const Baz
~const fn foo<T: Bar, S: Baz>() { ... }

// Function is not const.
// T could have a const or non-const implementation of Bar, likewise for S and Baz
fn foo<T: Bar, S: Baz>() { ... }
Details and examples

Applied to trait definitions:

// Trait is always const and must be implemented const
const trait Foo {};

// Trait may be const if it is implemented const
~const trait Foo {};

// Trait is never const
trait Foo {};

Users would then implement the trait using one of three blocks:

// The implementation is always const
impl const Foo for Bar {}

// The implementation may be const if possible (see below for discussion on this point)
impl ~const Foo for Bar {}

// The implementation is never const
impl Foo for Bar {}

When applied to bounds on functions, we get a side-effect in that the bounds on the trait can be entirely elided:

// Function is always const, therefore all trait implementations must always be const
const fn baz<T: Foo>() {}

// Function is const if all trait implementations are also const
~const fn baz<T: Foo>() {}

// Function is never const
fn baz<T: Foo>() {}

Users can still opt to use this proposal's trait bounds syntax where the requirements on a trait are stricter than the context of the signature:

// Non-const function requires that T has a const implementation of Foo
fn baz<T: const Foo>() {}

// Non-const function passing T into a ~const function. Allows compiler to use the const implementation if applicable
fn baz<T: ~const Foo>() {}

// Maybe-const function requires that T has a const implementation of Foo
~const fn baz<T: const Foo>() {}

This raises the question, how do you opt out of the implied const bound? I believe you'd never need to. Imagine T: !const Foo means means an implementation of Foo which is not const:

// ERROR: Non-const trait in const context
const fn baz<T: !const Foo>() {}

// Will never be const, so equivalent to fn baz<T: Foo>() {}
~const fn baz<T: !const Foo>() {}

// Seems pointless to me? Why would you want to declare code can't be const? Discuss!
fn baz<T: !const Foo>() {}

bushrat011899 avatar Jan 14 '25 00:01 bushrat011899

@bushrat011899 (and others who want to comment too): You'll probably get more success if you follow oli-obk's advice and use an inline comment so it can be threaded correctly and resolved when it's reached its mileage. I highly recommend you do that (and probably hide your original comment to dedupe it) just so people can reply to it more ergonomically.

I'll copy this if this ends up getting threaded.

// Function is const under all circumstances.
// This implies T: const Bar and S: const Baz
const fn foo<T: Bar, S: Baz>() { ... }

This is literally impossible to implement backwards compatibly. You can already write const fn foo<T: Bar, S: Baz>() today, and that doesn't require const impls for Bar or Baz.

compiler-errors avatar Jan 14 '25 01:01 compiler-errors

The RFC doesn't mention which traits in the standard library will be constified. I guess the goal is to constify all of them, but it might not be possible to constify traits that use unsupported features, e.g. heap allocation.

This is due to the main limitation of this proposal: To constify a trait, all methods have to be const. This also means that marking a trait as const is a big comment, since after that you can't add non-const default methods backwards compatibly.

I particularly care about Iterator, which has a lot of default methods, and has a big impact due to its use in for loops. I'd like to know if Iterator can be constified under this proposal.

Aloso avatar Jan 18 '25 15:01 Aloso

@Aloso Iterator should be possible with this RFC, however with how many methods it has that is quite difficult to do all at once.

AFAIK the current plan for constifying Iterator is to add a way to add a way to declare some trait methods as always non const with something like a rustc_non_const_trait_method attribute (see this issue). Not sure if that will be usable by non std code.

Deciding what exactly to constify in std is a Libs-API question and not really in scope for this PR.

onestacked avatar Jan 18 '25 15:01 onestacked

The RFC template has under Alternatives, the question:

  • What is the impact of not doing this?

Should this RFC have a section that reasons about the implications of NOT doing this?

For me on the sidelines, I see a complicated new syntax, but also don't know the big picture to where this takes us.

algesten avatar Jan 31 '25 09:01 algesten

The design has pivoted to replacing ~const with (const) and no more impl const Trait for Type or const Trait, instead the presence of const or (const) methods makes that implicit. See also https://rust-lang.zulipchat.com/#narrow/channel/328082-t-lang.2Feffects/topic/.22constantly.20effective.22/near/503902077 for background on where this change came from.

oli-obk avatar Mar 07 '25 12:03 oli-obk

As a random user, I think I prefer the alternative syntax of const meaning conditionally const, alongside =const meaning always const. It fits better with my intuition of what const means for functions, where the implementation and usage mean slightly different things.

  • As an implementation, impl const Trait for T currently implies that any (const) methods may be marked const, and so a theoretical impl =const Trait for T would imply that all (const) methods must be marked const.
  • As a bound these would imply the same thing; const means the methods may be const, and =const means they must be const.

So in the example:

const fn default<T: const Default>() -> T {
    T::default()
}

default would be usable in const contexts conditionally based on T's impl, but would always be usable in non-const contexts. Whereas in this version:

const fn default<T: =const Default>() -> T {
    T::default()
}

I'd expect that default only exists for const impls of Default for T. The parity makes it more intuitive I think, since you use the more verbose syntax for the less common situation (I'm just guessing =const would be less common).

Yokinman avatar Apr 09 '25 18:04 Yokinman

Please remember to create inline comments for discussions to keep this RFC manageable and discussion trees resolveable.

oli-obk avatar Apr 09 '25 18:04 oli-obk

Note: the following issue is marked deprecated (and locked)

  • https://github.com/rust-lang/rust/issues/67792

It says the tracking issue for the rewrite is:

  • https://github.com/rust-lang/rust/issues/110395

but that obviously only tracks the removal of the old version.

I don't really know what the feature is supposed to be, but it sorta seems like this RFC should have a new tracking issue, and maybe the old one should link to the new one instead of to the removal tracking issue?

SamB avatar Jun 07 '25 16:06 SamB

This RFC will get a new tracking issue once it gets accepted.

RalfJung avatar Jun 07 '25 16:06 RalfJung

There are currently plenty of discussions happening in the background behind closed doors that have been influencing implementations (for example, rust-lang/rust#139858 mentions that the lang team decided on a new syntax but there is no link to where that discussion happened)

When @oli-obk said, over in https://github.com/rust-lang/rust/pull/139858, that,

Afaict the syntax with which to move forward was settled as much as it could be in the last lang team design meeting. The lang team wants to get hands-on experience with the feature to see how it feels, so this PR is ready to be merged now.

he was referring to:

  • https://github.com/rust-lang/lang-team/issues/317

In that meeting, we didn't decide on the syntax. But, as Oli said, we need hands-on experience here, and along with the new proposal presented in that meeting and the lack of immediate strong objections, that's enough to move forward the experimental implementation work, as Oli did.

The doors aren't closed. Our meetings are open, and we keep extensive minutes that you can find linked from there.

traviscross avatar Jul 09 '25 03:07 traviscross

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.

RalfJung avatar Jul 09 '25 06:07 RalfJung

Agree of course we should properly track things. In my view, https://github.com/rust-lang/rust/issues/67792 is still the proper tracking issue for this work (as with many of our tracking issues, of course, it could use a lot of love to be brought up to date). I've gone ahead and unlocked it. I'd expect the reason we needed to lock it was temporary and hopefully the need for that has passed. If that's not the case, we can always lock it again.

traviscross avatar Jul 09 '25 06:07 traviscross

The doors aren't closed. Our meetings are open, and we keep extensive minutes that you can find linked from there.

I should clarify, I wasn't saying that the information wasn't technically available, just that it wasn't immediately obvious where to look. For example, even among lang team meetings, it's not immediately clear which one it took place in, and the more time that passes between when a decision was made and when the decision is noticed by someone else, the harder it is to find which specific discussion occurred if it's not linked. It's entirely plausible that the discussion happened multiple meetings before the PR and it was only at that point where someone got around to implementing it.

This is kind of the reason why tracking issues exist: instead of linking every micro-discussion that happened elsewhere when it comes to important changes with respect to a thing, the unified tracking issue tracks any relevant decisions that happened. Precisely so you don't have to look through a bunch of irrelevant discussions in meeting notes, you can just have a bullet point in the tracking issue pointing out a PR that was made, which mentions in its description that it was after a discussion in a lang team meeting.

(Thank you for unlocking the issue, by the way; I'm mostly just adding a bit more context since I'm not 100% sure my point was clear.)

clarfonthey avatar Jul 10 '25 04:07 clarfonthey

I don't know if this is open for discussion anymore, but I just wanted to say that I find the syntax using square brackets in [const] somewhat awkward. In other parts of Rust square brackets already mean the creation or indexing of some container, so using them here to indicate another thing alltogether feels like its overloading the meaning of square brackets too much.

I much preferred the old ~const syntax. It also had a pleasing symmetry with the other prefix modifiers like ?Trait and !Trait.

FeldrinH avatar Jul 27 '25 05:07 FeldrinH

I was playing around with this today. And I noticed a potential inconsistency. In trait definitions, you have this theoretical syntax:

const unsafe trait Foo { ... }

But in implementation, you have this theoretical syntax:

unsafe impl const Foo ...

Perhaps it should be this?

const unsafe impl Foo ...

I suspect const is a property of the implementation not of the trait -- precisely the same as unsafe.

npmccallum avatar Aug 09 '25 21:08 npmccallum

@npmccallum There is the precedent of unsafe const fn. Wouldn't it be more consistent to go with unsafe const impl then?

But then there's still the issue that the RFC proposes impl const Trait, so only unsafe impl const Trait would be consistent with that...

teohhanhui avatar Aug 09 '25 22:08 teohhanhui

@npmccallum There is the precedent of unsafe const fn. Wouldn't it be more consistent to go with unsafe const impl then?

But then there's still the issue that the RFC proposes impl const Trait, so only unsafe impl const Trait would be consistent with that...

$ 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

npmccallum avatar Aug 09 '25 22:08 npmccallum

Also, https://github.com/rust-lang/rust/pull/143879 puts const before unsafe.

To be clear, I don't care about the ordering of const vs unsafe. I'm only pointing out that unsafe trait requires unsafe impl but const trait requires impl const. That seems inconsistent to me.

npmccallum avatar Aug 09 '25 22:08 npmccallum

In light of that, from what I can tell the syntax needs to be either:

  1. const impl Trait / const unsafe impl Trait
  2. impl const Trait / impl const unsafe Trait
  3. impl const Trait / unsafe impl const Trait

to be self-consistent.

2 is impossible because the existing syntax is unsafe impl.

teohhanhui avatar Aug 09 '25 23:08 teohhanhui

I suspect const is a property of the implementation not of the trait -- precisely the same as unsafe.

const is part of the identification of the interface being implemented: “this impl block implements the const version of the trait”. unsafe is a modifier for the implementation itself: “I assert that this impl block fulfills the safety preconditions for implementing the trait”. It makes perfect sense, from that perspective, for unsafe to precede impl but const to precede the trait.

Jules-Bertholet avatar Aug 10 '25 00:08 Jules-Bertholet

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.

clarfonthey avatar Aug 10 '25 22:08 clarfonthey

https://github.com/rust-lang/rfcs/pull/3762#issuecomment-3124038598:

I don't know if this is open for discussion anymore, but I just wanted to say that I find the syntax using square brackets in [const] somewhat awkward. In other parts of Rust square brackets already mean the creation or indexing of some container, so using them here to indicate another thing alltogether feels like its overloading the meaning of square brackets too much.

I much preferred the old ~const syntax. It also had a pleasing symmetry with the other prefix modifiers like ?Trait and !Trait.

@oli-obk Any comment on this? Is the [const] syntax final?

FeldrinH avatar Aug 19 '25 02:08 FeldrinH

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

oli-obk avatar Aug 19 '25 06:08 oli-obk

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.

FeldrinH avatar Aug 20 '25 15:08 FeldrinH

I believe that the [const] syntax was specifically decided in a lang team meeting, and as far as I'm aware, it's been substantially less discussed than the former ~const syntax. That doesn't mean it's necessarily better or worse, but I do think that a lot of people, myself included, have absolutely no idea what the justification was for it beyond "lang team wanted to try it out."

I get that these discussions are exhausting, and I don't think that syntax should necessarily block this RFC, but I do think that it's worthwhile collecting all those discussions in one place, rather than just asking people to scour Zulip for them. That is the purpose of an RFC, after all: explaining everything in one place so people don't have to piece everything together themselves from several fragmented discussion threads.

clarfonthey avatar Aug 21 '25 02:08 clarfonthey

Nothing has been decided. My understanding is that there are some people on the lang team who like [const], but there is no consensus, and it hasn’t been discussed much in lang team meetings. The current nightly impl supports both, so people can experiment with whichever syntax they prefer.

I’m sympathetic to the concern of wanting discussions out in the open, but I don't think it makes sense to burden the GitHub discussion with endless conversation threads at this stage, while things remain so open-ended.

Jules-Bertholet avatar Aug 21 '25 05:08 Jules-Bertholet

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.

clarfonthey avatar Aug 21 '25 05:08 clarfonthey