# Quadrants? 2025-04-16 (See also a post on the earlier model at https://hackmd.io/@beef/HkhwPl121e) Under the quadrant model, we write a const trait by turning the following ```rust pub trait PartialEq<Rhs: ?Sized = Self> { fn eq(&self, other: &Rhs) -> bool; fn ne(&self, other: &Rhs) -> bool { !self.eq(other) } } impl PartialEq for () { fn eq(&self, other: &()) -> bool { true } } ``` into ```rust pub const trait PartialEq<Rhs: ?Sized = Self> { (const) fn eq(&self, other: &Rhs) -> bool; (const) fn ne(&self, other: &Rhs) -> bool { !self.eq(other) } } impl const PartialEq for () { (const) fn eq(&self, other: &()) -> bool { true } } ``` Let's discuss whether this model should be the one we choose. Note that the `const` in `pub const trait` and `impl const PartialEq` as a requirement was justified in my previous [HackMD doc](https://hackmd.io/@beef/HkhwPl121e), and there was agreement that that should be the case. If you would like to refute the assumption that those keywords should be applied to the `impl` and `trait` levels, please refute the argument in the previous doc. ## Duck Language Design Let's take some time to explain the language feature as it stands. Why don't we use a Q&A format as well? Okay let's go. **Q1.** What is a trait? **A1.** A trait specifies common properties and functionalities between multiple types. **Q2.** How do you define a trait? **A2.** The visibility of the trait, its constness (if it is const, write `const`, if not, write nothing), its name, its generic bounds, then its items are contained within its block. **Q3.** Tell me more about constness. **A3.** A trait is `const` if the properties can hold or the functionalities can be called in compile time. `impl`s can decide whether that is the case for specific types, so `impl`s can either be `impl Trait` or `impl const Trait`. **Q4.** Great! So if we have `const trait Trait`, it means that its methods can be used in compile time if a type has `impl const Trait for Type`. **A4.** Not quite. For you to indicate that a method inside a `const trait` can be used in compile time, you must add `(const)` in front of the method, such as `(const) fn foo(&self);`. **Q5.** Tell me more about `(const)`. **A5.** (Do I have to have an advanced academic background in effects systems to explain this? Let's try to keep it simple anyways.) It's a way to indicate that a method can either be implemented as callable from const contexts or can only be called from non-const contexts, depending on the implementation. The parentheses mean it is optional. **Q6.** So `impl`s don't really need to allow those methods to be callable from const contexts? **A6.** It depends. If an `impl` is `const`, then it must implement all `(const) fn` defined on the trait as `(const) fn` in the `impl` as well. Otherwise, there is no requirement on what the `impl` does. **Q7.** You said the parentheses means it is optional. Why would an impl method have an optional constness? **A7.** It means that the `impl` can either be `const` or not-`const` depending on whether bounds are satisfied. For example: ```rust pub const trait Trait<T> { (const) fn foo(x: &T, y: &T) -> bool; } impl<T: (const) PartialEq> const Trait<T> for () { (const) fn foo(x: &T, y: &T) -> bool { x == y } } ``` The `foo` method is only `const` when `T: const PartialEq` is met. The parenthesis ties the constness of that method to the constness of the bounds. **Q8.** A `const fn` without attachment to a trait can also have bounds right? Why don't we write `(const) fn` instead? Why do we also write `const Trait` and not `(const) Trait`? **A8.** I'm not sure. Perhaps this is planned for a future edition? **Q9.** Okay, let's go back to my original assumption in Q4. There's the additional requirement that methods must be annotated with `(const)`. But the `const trait` part applies to the entire trait and tells us that the functionality/property can be used in const contexts. What would methods without `(const)` mean? **A9.** It would mean that those methods are never callable from const contexts. As a trait defines a common functionality/property between types, a method without `(const)` suggests that the method does not need to be callable from const contexts for the `impl` to satisfy that functionality/property _in const contexts_. **Q10.** Are there any real world cases where this separation and distinction is needed? **A10.** I'm not sure. ---- We're back to blog voice now. Did the conversation feel a little awkward and long-winded? It was specifically written for someone who has a decent knowledge about Rust but is learning about this feature the first time. But we actually get pretty quickly to a lot of problems (to me) with the current design! ## In search of justification (and use cases) Let's summarize some of the problems we discovered above. 1. (Q4, A4) It feels weird that appending `const` to `trait` doesn't actually do anything. It is useful as a transition indicator to make changes that would be later breaking changes. Other reasons for its usefulness are in the linked doc above. 2. (A5) It is already slightly hard to talk about `(const)` as it stands, especially given its unique position of being placed on only trait methods, impl methods, and trait bounds. 3. (Q6, A6) Non-intuitiveness about the optional-ness actually being required when you're writing a `impl const Trait` 4. (Q7) It may seem intuitive that `(const)` stands for optionally-const. It makes sense for requirements to be annotated like so, such as methods on a trait `(const) fn` or a trait bound, `T: (const) Trait`, it is less intuitive to put on methods inside an `impl`. To my best knowledge `(const)` is an alternative approximation to `~const`. 5. (Q8, A8) Lack of consistency across the board considering syntax for everything 6. (A9) Hard to explain a regular `fn foo` inside a `const trait Trait` and what it means 7. (Q10, A10) No realistic use case for the thing described in A9. People can still split traits. These problems centers around the teachability aspect of the current model, and less about whether it "makes sense" in accordance to formal semantics. I don't have a background in formal semantics so I am unable to provide opinions on that matter. ## Towards a better proposal To be clear, every potential const traits proposal must leave space for a future where more optional things on a trait get added to the language. For example a trait can be optionally async. Or can be optionally nopanic. Etc. My main focus for developing the next proposal is centered around the last few questions in the conversation above. Namely: if _effects_ are supposed to be trait-level things (in plain words: if an entire trait can decide whether or not it makes sense to distinguish its functionality/properties between compile time and runtime or between async and sync contexts, etc.), then does it ever make sense for individual methods to opt-out of such things? My answer to that is largely no, and it stems from one definition of traits. For example, `T: Iterator` tells me that `T` has the property of being an `Iterator`. `T: const Iterator` tells me that `T` has the property of being an `Iterator` in compile time contexts. Same goes for a futuristic syntax such as `T: async Iterator`. All of these are type/trait level reasoning. It feels much more reasonable that all methods of such trait observe this, rather than allowing exceptions to be made. There are more reasons, but I'm mainly curious about potential reasons for why we _should_ allow these exceptions. ## The 2025-04-16 proposal _because naming things is not my best skill_ Turn a trait to const by changing ```rust pub trait PartialEq<Rhs: ?Sized = Self> { fn eq(&self, other: &Rhs) -> bool; fn ne(&self, other: &Rhs) -> bool { !self.eq(other) } } impl PartialEq for () { fn eq(&self, other: &()) -> bool { true } } ``` into ```rust pub (const) trait PartialEq<Rhs: ?Sized = Self> { fn eq(&self, other: &Rhs) -> bool; fn ne(&self, other: &Rhs) -> bool { !self.eq(other) } } impl const PartialEq for () { fn eq(&self, other: &()) -> bool { true } } ``` 1. Q4, A4: We actually give the transition a meaning. Adding `(const)` means this trait is now _optionally_-const - it can be used in compile time for different `impl`s. If an `impl` does it for `const`, then _all_ the methods are required to be callable in compile time (at least when satisfying bounds of the `impl`) 2. A5, Q6, A6, Q7: `(const)` is now only used for _requirements_. A `(const) trait` represents a requirement (for its impls), and a `T: (const) Trait` bound represents a requirement (for `T`) 3. Q8, A8: `const fn` and `impl const` are now unified 4. A9, Q10, A10: Simply disallow such method-level exceptions to be made. And we can elaborate into some future trait definition where one would write using split traits to express something the quadrants model explicitly carved out: ```rust pub (const, async) trait Foo { fn uwu(); } pub (const) trait FooBlocking: (const) Foo { fn something_that_can_never_be_async(); } impl const async Foo for () { // ... } impl const FooBlocking for () { // ... } ``` Another example: ```rust impl<T: (const, async) Iterator, F: (const, async) Fn(T::Item) -> O, O> const async Iterator for Map<T, F> { type Item = O; fn next(&mut self) -> Option<O> { // figure something out with the closures here self.inner.next().map(|x| (self.fun)(x)) } } ``` Here's how the conversation would go in the 2025-04-16 proposal: **Q1.** What is a trait? **A1.** A trait specifies common properties and functionalities between multiple types. **Q2.** How do you define a trait? **A2.** The visibility of the trait, its constness (if it is const, write `(const)`, if not, write nothing), its name, its generic bounds, then its items are contained within its block. **Q3.** Tell me more about constness. **A3.** We add `(const)` to traits if the properties can hold or the functionalities can be called in compile time. `impl`s can decide whether that is the case for specific types, so `impl`s can either be `impl Trait` or `impl const Trait`. **Q4.** Great! So if we have `(const) trait Trait`, it means that its methods can be used in compile time if a type has `impl const Trait for Type`. **A4.** Yep! All methods inside an `impl const Trait` are expected to be able to be called from compile time. **Q5.** Tell me more about `(const)`. **A5.** The parentheses mean it is optional. The `impl` decides what to implement. This is similar to bounds such as `T: (const) Trait`. **Q6.** Hmm. Tell me more about those bounds. **A6.** Since a `const fn` can be called both from compile time and from runtime, the bound on `const fn compare<T: (const) PartialEq>() {}` isn't a strict requirement. For a caller in runtime, that bound does not need to be satisfied. **Q7.** Following that logic, why don't we say `(const) fn compare` then? **A7.** Good point. It tells people that the function does not need to be called from const contexts. However, from the `(const)`-as-an-optional-requirement point of view, the `(const) fn compare` isn't about something a caller would _satisfy_, as an `impl const Trait` for `(const) trait Trait` would or `T = ()` for `T: (const) Trait` would. To make the language even more consistent, we can definitely explore something like that in the next edition, to also prepare us to unify the maybe-async front, so that the examples about the far future above could be written as: ```rust impl (const, async) Foo for () { // ... } impl<T: (const, async) Iterator, F: (const, async) Fn(T::Item) -> O, O> (const, async) Iterator for Map<T, F> { type Item = O; fn next(&mut self) -> Option<O> { // figure something out with the closures here self.inner.next().map(|x| (self.fun)(x)) } } ``` just to keep everything consistent since `async fn` means always async, while `const fn` currently means maybe const. Note that we covered a lot more things in the conversation about the revised proposal with less explanation needed.