--- title: "Design meeting 2021-10-13: Where the where" tags: "design meeting" --- # Design meeting 2021-10-13: Where the where Issue: https://github.com/rust-lang/rust/issues/89122 [Source](https://rust-lang.github.io/generic-associated-types-initiative/design-discussions/where-the-where.html) First brought up on zulip: https://rust-lang.zulipchat.com/#narrow/stream/144729-wg-traits/topic/GAT.20syntax.20whining ## Summary Proposed: to alter the syntax of where clauses on type aliases so that they appear *after* the value: ``` type StringMap<K> = BTreeMap<K, String> where K: PartialOrd ``` This applies both in top-level modules and in traits (associated types, generic or otherwise). ## Background The current syntax for where to place the "where clause" of a generic associated types is awkward. Consider this example ([playground](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=bdb55a5d5cb17e20d73e22a3f2db0e57)): ```rust trait Iterable { type Iter<'a> where Self: 'a; fn iter(&self) -> Self::Iter<'_>; } impl<T> Iterable for Vec<T> { type Iter<'a> where Self: 'a = <&'a [T] as IntoIterator>::IntoIter; fn iter(&self) -> Self::Iter<'_> { self.iter() } } ``` Note the impl. Most people expect the impl to be written as follows (indeed, the author wrote it this way in the first draft): ```rust impl Iterable for Vec<T> { type Iter<'a> = <&'a [T] as Iterator>::Iter where Self: 'a; fn iter(&self) -> Self::Iter<'_> { self.iter() } } ``` However, this placement of the where clause is in fact rather inconsistent, since the `= <&'a [T] as Iterator>::Iter` is in some sense the "body" of the item. The same current syntax is used for where clauses on type aliases ([playground](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=74eeed1795b693f238150f825a0e8438)): ```rust type Foo<T> where T: Eq = Vec<T>; fn main() { } ``` ## Top-level type aliases Currently, we accept where clauses in top-level type aliases, but they are deprecated (warning) and semi-ignored: ``` type StringMap<K> where K: PartialOrd = BTreeMap<K, String> ``` Under this proposal, this syntax remains, but is deprecated. The newer syntax for type aliases (with `where` coming after the type) would remain feature gated until such time as we enforce the expected semantics. ## Interaction with trait aliases One thing discussed in the thread was the interaction with trait alias syntax. With trait aliases, the where clauses can appear in different positions relative to `=` with different meanings: ```rust // To use this alias, `T: Bar` must hold trait Foo<T> where T: Bar = ... // `Foo<T>` is an alias for `T: Bar` trait Foo<T> = ... where T: Bar ``` However, this is related to another point about GAT syntax: ```rust trait Foo { // To use Bar, T: Ord must hold type Bar<T: Ord>; // Impl must prove that `Self::Bar<T>: Ord`, // and other users can rely on that type Bar<T>: Ord; } ``` There is no syntax with GATs today to add "impl must prove" constraints like `T: Ord`. Perhaps if there were, it might apply to trait aliases, too? ## Alternatives ### Keep the current syntax. In this case, we must settle the question of how we expect it to be formatted (surely not as I have shown it above). ```rust impl<T> Iterable for Vec<T> { type Iter<'a> where Self: 'a = <&'a [T] as IntoIterator>::IntoIter; fn iter(&self) -> Self::Iter<'_> { self.iter() } } ``` ### Accept either What do we do if both are supplied? ### Questions / Discussion ### Q > Mark: I'm not sure I understand the distinction between the following two (from the trait alias section). Isn't it true in both that the `T: Bar` bound must hold? > * is there a concise explanation somewhere for this? It feels like a critical element but also not something obvious (to me, anyway). ```rust trait SendSync: Send + Sync ``` What about something more complex? [RFC #1733](https://rust-lang.github.io/rfcs/1733-trait-alias.html) gave this syntax: ```rust trait RevPartialEq<T> = where T: PartialEq<Self>; ``` To illustrate difference: ```rust // OK trait RevPartialEq<T> = where T: PartialEq<Self>; fn test<X: RevPartialEq<u32>>(x: X) { 22_u32 == x } ``` ```rust // Error: `u32: PartialEq<T>` does not hold // // Related to implied bounds, though. trait Test<T> where T: PartialEq<Self> = Debug; fn test<X: Test<u32>>(x: u32) { 22_u32 == x } ``` just as if I had written: ```rust // just as if I had written: // // trait Test<T> // where T: PartialEq<Self> // { } ``` ```rust trait Test<T> where Self: PartialEq<T> { } // No error: `Self: PartialEq<u32>` is considerd a "super trait" // and hence part of "implied bounds". fn test<X: Test<u32>>(x: u32) { 22_u32 == x } ``` No way to make an implied bound that doesn't begin with Self. ```rust trait DebugIterator { type Item: Debug; // Knowing that X: DebugIterator implies X::Item: Debug } ``` No way to make an implied bound that doesn't begin with Self. ```rust trait DebugIterator { type Item<T>: Debug; // What if I wanted `T: Debug<U>` as an implied bound? // Does that even make sense?? } ``` Niko: I feel the trait alias syntax is just confusing Mark: Is there a reason to have both kinds of where clauses in traits? What if you only had implied bounds with trait aliases? (Or only the reverse...) Felix: Does it have any impact on quality of dev ex? * Maybe you get an error that points to a better line of code? We don't know. Jack: As a data point, there was at least one issue with GATs where they were using where clauses but shouldn't have been (or maybe it was the other way around...) but I don't know what the right syntax would be. (Update: I think this was the issue: https://github.com/rust-lang/rust/issues/87831) Niko: I remember this, I think they were putting the where clause on the trait maybe? ### Q > Felix: is part of expectation here that `cargo fix` or `rustfmt` will auto-convert the ugly syntax to the nice one? (This ties into my Q above regarding the increase in severity for parametric type aliases.) ((or maybe type aliases would be excluded from auto-suggested change)) niko: cargo fix for sure. For rustfmt, if we said that it was an error in GATs, then it would be new ground, right? Accepting a superset of rust and emitting rust? But I don't see any fundamental reason why not. pnkfelix: if you convert type aliases, you are going from a where clause that gets ignored to one that gets enforced. niko: yeah we'd need some opt-in of some kind. ### Q > * Felix: In the "accept either" option: I assume we will always have to accept the old syntax, at least in old editions. > * so it makes some sense to continue accepting either. > * but that does *not* mean we have to accept both in tandem on the same item. That makes sense, at least for top-level type aliases. ### Q > * Scott: Random thought: `WHERE` and `HAVING` being different in SQL is also confusing to people, so I don't know if different keywords would necessarily be better. Probably still better than it being position-dependent, though. Niko: egads I forgot what those mean Scott: HAVING comes after the GROUP BY, and you can reference difference things; it applies to the results of your projection. Niko: I kind of agree that unless the keywords are *very well chosen* it won't help. Scott: Is there some way to leverage the same thing twice somehow to get the semantics, vs two different keywords? Niko: Do people even want that distinction? When discussing implied bounds, there was discussion about how removing where clauses becomes semver significant. Almost a private vs a public bound. Do you get to rely on it, or does everyone else get to rely on it too? ### Q > * Scott: we have `struct Foo<T>(T) where T: Copy;` but `struct Foo<T> where T: Copy { x: T }`. Does anyone remember why those are different? Is there a general rule there that we can divine from there that would help us pick here? Something about braces? Niko: I don't recall it being discussed a lot, but the general rule was that the first one looked like a function. [RFC 0135](https://rust-lang.github.io/rfcs/0135-where.html) Scott: Seems like it has to do with braces enclosing a lot of content, but the parens were short... maybe that says that the type is "one thing", not a multi-line whatever, so it is more like the tuple struct case. Niko: I feel that way, it feels more like the fn or tuple struct case to me. Scott: It feels like it would be weird to have the where clause list before the parameter list or `->` in a function, although you could have it there if you wanted. Niko: The original RFC argued that function parameters and return types were more important than the where clauses, and that the bounds were a kind of footnote. I think you can make an analogous case here. Niko: Does anyone want to argue *against* the proposed where clause syntax? Scott: I'm in favor, but I'm trying to figure out *why*. Jack: It's very subjective. Everybody seems to agree that the where clauses should go at the end because of formatting, but it's very subjective. Arguments for the current syntax (where before =): * Consistency of 'copying and pasting' an item from trait and appending 'the definition' (which in this is the value of the type alias). * Trait alias subtleties. Arguments against (iow, for where after =): * Consistency with function and tuple struct placement * Note that we initially had tuple structs put the where before the `()` ([link](https://github.com/rust-lang/rust/issues/17904#issuecomment-58603749)) but it was "obviously wrong" somehow * The thing after the where clauses (if any) should start on its own line, and we don't have precedent for `=` starting on its own line * where clauses are a "big long list" of things and you want it to be easy to find the end * Calls attention to the value of the type alias vs the bounds * can put them in the angle brackets to emphasize them (most of the time) * ["secondary notation"](https://en.wikipedia.org/wiki/Cognitive_dimensions_of_notations) works better, more able to draw a distinction There's a reason that rustfmt behaves like so: ```rust let x = long_expression; // Becomes let x = long_expression; // Not let x = long_expression; ``` Example of issue with "long" where clauses: https://github.com/rust-lang/rust/issues/86787 ```rust type T = Either<Left::T, Right::T>; type TRef<'a> where <Left as HasChildrenOf>::T: 'a, <Right as HasChildrenOf>::T: 'a = Either<&'a Left::T, &'a Right::T>; type T = Either<Left::T, Right::T>; type TRef<'a> where <Left as HasChildrenOf>::T: 'a, <Right as HasChildrenOf>::T: 'a = Either<&'a Left::T, &'a Right::T>; type T = Either<Left::T, Right::T>; type TRef<'a> = Either<&'a Left::T, &'a Right::T> where <Left as HasChildrenOf>::T: 'a, <Right as HasChildrenOf>::T: 'a; // Scott: but ending a multiline list with commas? // Niko: how is it different from tuple structs? struct Foo<T>(u32) where T: Ord; // Yup, checked, and rustfmt does struct Foo<T, U>(T, U) where T: Ord, U: Ord; ``` ### Q Would implied bounds even make sense on GATs? ```rust trait MyTrait { type MyType<T> = where T: PartialEq<Self::MyType<T>>; } impl MyTrait for .. { type MyType<T> = ValueType where T: PartialEq<Self::MyType<T>>; } impl<A> PartialEq<ValueType> for A { } ``` It is true that * if we do want a syntax for this * and it is the trait alias syntax we just messed it up. - Comment from Jack - But actually the "implied bound" where clause is in front of the equals, and this is not that ### Q > Scott: Do we need people to copy them over? Is it a breaking change to remove them? Does the impl need to repeat them? Today they are implied, but we've also discussed this proposed change: ```rust trait Foo { unsafe fn foo(); } impl Foo for () { fn foo(); } fn safe_fn() { <() as Foo>::foo(); // No unsafe needed } ``` "If the compiler can identify your impl, it can use the signature for your impl" -- should that work just for unsafe, but not for where clauses? Seems weird. Scott: What about lifetimes? Niko: Currently everything is typed against the trait signature. I would expect that to be consistent with unsafe but it occurs to be that there is more of a circularity implementation wise. * Today: * Consumers of the associated type would ignore the bounds on the impl, so they could be defaulted to the ones that appear in the trait. * Tomorrow: * But if we made it significant that you use fewer, that would be a change, and in particular having an empty list would make code stop compiling if those predicates were required for the value to be well-typed. But of course we could do this over an edition easily enough.