diff --git a/0000-friends_in_high_kindednesses.md b/0000-friends_in_high_kindednesses.md index bb74d560d58..bd31e696ab0 100644 --- a/0000-friends_in_high_kindednesses.md +++ b/0000-friends_in_high_kindednesses.md @@ -1,4 +1,4 @@ -- Feature Name: associated_type_operators +- Feature Name: associated_type_constructors - Start Date: 2016-04-29 - RFC PR: (leave this empty) - Rust Issue: (leave this empty) @@ -6,14 +6,14 @@ # Summary [summary]: #summary -Allow type operators to be associated with traits. This is an incremental step -toward a more general feature commonly called "higher-kinded types," which is -often ranked highly as a requested feature by Rust users. This specific feature -(associated type operators) resolves one of the most common use cases for -higher-kindedness, is a relatively simple extension to the type system compared -to other forms of higher-kinded polymorphism, and is forward compatible with -more complex forms of higher-kinded polymorphism that may be introduced in the -future. +Allow type constructors to be associated with traits. This is an incremental +step toward a more general feature commonly called "higher-kinded types," which +is often ranked highly as a requested feature by Rust users. This specific +feature (associated type constructors) resolves one of the most common use +cases for higher-kindedness, is a relatively simple extension to the type +system compared to other forms of higher-kinded polymorphism, and is forward +compatible with more complex forms of higher-kinded polymorphism that may be +introduced in the future. # Motivation @@ -39,10 +39,10 @@ rather than for as long as the borrow initiated by `next`. This trait cannot be expressed in Rust as it exists today, because it depends on a sort of higher-kinded polymorphism. This RFC would extend Rust to include that specific form of higher-kinded polymorphism, which is refered to here as -associated type operators. This feature has a number of applications, but the -primary application is along the same lines as the `StreamingIterator` trait: -defining traits which yield types which have a lifetime tied to the local -borrowing of the receiver type. +associated type constructors. This feature has a number of applications, but +the primary application is along the same lines as the `StreamingIterator` +trait: defining traits which yield types which have a lifetime tied to the +local borrowing of the receiver type. # Detailed design [design]: #detailed-design @@ -50,17 +50,18 @@ borrowing of the receiver type. ## Background: What is kindedness? "Higher-kinded types" is a vague term, conflating multiple language features -under a single inaccurate banner. Let us discuss specifically the notion of a -'kind' as background for this RFC. Kinds are often called 'the type of a type', -the exact sort of unhelpful description that only makes sense to someone who -already understands what is being explained. We'll take a different approach. +under a single banner, which can be inaccurate. As background, this RFC +includes a brief overview of the notion of kinds and kindedness. Kinds are +often called 'the type of a type,' the exact sort of unhelpful description that +only makes sense to someone who already understands what is being explained. +Instead, let's try to understand kinds by analogy to types. In a well-typed language, every expression has a type. Many expressions have what are sometimes called 'base types,' types which are primitive to the language and which cannot be described in terms of other types. In Rust, the types `bool`, `i64`, `usize`, and `char` are all prominent examples of base -types. In contrast, there are other types which are formed by arranging other -types - functions are a good example of this. Consider this simple function: +types. In contrast, there are types which are formed by arranging other types - +functions are a good example of this. Consider this simple function: ```rust fn not(x: bool) -> bool { @@ -68,202 +69,376 @@ fn not(x: bool) -> bool { } ``` -`not has the type `bool -> bool` (my apologies for using a syntax different +`not` has the type `bool -> bool` (my apologies for using a syntax different from Rust's). Note that this is different from the type of `not(true)`, which -is `bool`. This difference is important, by way of analogy, to understanding -higher-kindedness. +is `bool`. This difference is important to understanding higher-kindedness. In the analysis of kinds, all of these types - `bool`, `char`, `bool -> bool` -and so on - have the kind `type`, which is often written `*`. This is a base -kind, just as `bool` is a base type. In contrast, there are more complex kinds, -such as `* -> *`. An example of an term of this kind is `Vec`, which takes a -type as a parameter and evalues to a type. The difference between the kind of -`Vec` and the kind of `Vec` (which is `*`) is analogous to the difference -between the type of `not` and `not(true)`. Note that `Vec` has the kind `*`, -just like `Vec`: even though `T` is a type parameter, `Vec` is still being -applied to a type, just like `not(x)` still has the type `bool` even if `x` is -dynamically determined. +and so on - have the kind `type`. Every type has the kind `type`. However, +`type` is a base kind, just as `bool` is a base type, and there are terms with +more complex kinds, such as `type -> type`. An example of a term of this kind +is `Vec`, which takes a type as a parameter and evaluates to a type. The +difference between the kind of `Vec` and the kind of `Vec` (which is +`type`) is analogous to the difference between the type of `not` and +`not(true)`. Note that `Vec` has the kind `type`, just like `Vec`: even +though `T` is a type parameter, `Vec` is still being applied to a type, just +like `not(x)` still has the type `bool` even though `x` is a variable. A relatively uncommon feature of Rust is that it has _two_ base kinds, whereas -many languages which deal with higher-kindedness only have the base kind `*`. -The other base kind of Rust is the lifetime parameter, which for conveniences -sake we will represent as `&`. For a type `Foo<'a>`, the kind of `Foo` is -`& -> *`. - -Terms of a higher kind are often called 'type operators'; type operators which -evaluate to a type are called 'type constructors.' The concept of -'higher-kinded types' usually refers to the ability to write code which is -polymorphic over type operators in some way, such as implementing a trait for a -type operator. This proposal is to allow a type operator to be associated with -a trait, in the same way that a type or a const can be associated with a trait -today. - -## The basic requirements of associated type operators - -Adding associated type operators to the language requires the introduction of -four discrete constructs: - -1. In a definition of a trait, an associated type operator can be declared. -2. In the position of any type within the definition of that trait, the - associated type operator can be applied to a type parameter or a concrete - type which is in scope. -3. In the implementation of that trait, a type operator of the correct kind can - be assigned to the declared associated type operator. -4. When bounding a type parameter by that trait, that trait can be bound to - have a concrete type operator as this associated type operator. - -## Partial application +many languages which deal with higher-kindedness only have the base kind +`type`. The other base kind of Rust is the lifetime parameter. If you have a +type like `Foo<'a>`, the kind of `Foo` is `lifetime -> type`. -In order for this feature to be useful, we will have to allow for type -operators to partially applied. Many languages with higher-kinded polymorphism -use currying as an alternative to partial application. Rust does not have -currying at the level of expressions, and currying would not be sufficient -to enable the use cases that exist for type operators, so this RFC does not -propose using currying for higher-kinded polymorphism. - -As an example, the reference operator has the kind `&, * -> *`, taking both -a lifetime and a type to produce a new type. With currying, the two parameters -to the reference operator would have to have a defined order, and it would -be possible to partially apply only one of the parameters to the reference -operator. That is, if it were `& -> * -> *`, one could apply it to a lifetime -to produce a `* -> *` operator, but one could not apply it to a type to produce -a `& -> *` operator. If it were defined as `* -> & -> *`, it would be -restricted in the opposite way. Because Rust makes use of two base kinds, -currying would severely restrict the forms of abstraction enabled by Rust. - -Instead, when defining an associated type operator, an anonymous type operator -can be constructed from a type operator with more parameters by applying any -of the parameters to that operator. The syntax discussed below makes it -unambiguous and easy to see which parameters remain undetermined at the point -of assigning the associated type operator to a concrete type operator. +Higher-kinded terms can take multiple arguments as well, of course. `Result` +has the kind `type, type -> type`. Given `vec::Iter<'a, T>` `vec::Iter` has the +kind `lifetime, type -> type`. -When used in a type position, of course, all of the parameters to an associated -type operator must have been applied to concrete types or type parameters that -are in scope. +Terms of a higher kind are often called 'type operators'; the type operators +which evaluate to a type are called 'type constructors'. There are other type +operators which evaluate to other type operators, and there are even higher +order type operators, which take type operators as their argument (so they have +a kind like `(type -> type) -> type`). This RFC doesn't deal with anything as +exotic as that. + +Specifically, the goal of this RFC is to allow type constructors to be +associated with traits, just as you can currently associate functions, types, +and consts with traits. There are other forms of polymorphism involving type +constructors, such as implementing traits for a type constructor instead of a +type, which are not a part of this RFC. -## Associated type operators in bounds +## Features of associated type constructors -This RFC proposes making associated type operators available in bounds only -as concrete type operators. Because higher-kinded traits cannot be defined, and -traits cannot be implemented for type operators, it is not possible to bound -associated type operators by traits. +### Declaring an associated type constructor -Even without higher-kinded traits, it could be useful to bound associated type -operators with some sort of higher-rank syntax, as in: +This RFC proposes a very simple syntax for defining an associated type +constructor, which looks a lot like the syntax for creating aliases for type +constructors. The goal of using this syntax is to avoid to creating roadblocks +for users who do not already understand higher kindedness. ```rust -T where T: StreamingIterator, for<'a> T::Item<'a>: Display +trait StreamingIterator { + type Item<'a>; +} ``` -However, this RFC does not propose adding this feature. +Here, it is clear that `Item` is a type constructor, because it carries a +parameter. Associated type constructors can carry any number of type and +lifetime parameters, as in: -## Benefits of implementing only this feature before other higher-kinded polymorphisms +```rust +trait FooBar { + type Baz<'a, T, U>; +} +``` -This feature is the first 20% of higher-kinded polymorphism which is worth 50% -of the full implementation. It is the ideal starting point, as it will enable -many constructs while adding relatively few complicates to the type system. By -implementing only associated type operators, we sidestep several issues: +Associated type constructors can be followed by `where` clauses, which place +trait bounds on the types constructed by this constructor. For example: -* Defining higher-kinded traits -* Implementing traits for type operators -* Higher order type operators -* Type operator parameters bound by higher-kinded traits -* Type operator parameters applied to a given type or type parameter +```rust +trait Collection { + type Iter<'a> where for<'a> Self::Iter<'a>: Iterator; + type IterMut<'a> where for<'a> Self::IterMut<'a>: Iterator; + type IntoIter: Iterator; +} +``` + +A `where` clause is used to avoid the impression that this is providing a +bound on the constructor itself. Note the contrast to `IntoIter`, which is +not a type constructor. Also note that this involves an extension to HRTB, +which is discussed later in this RFC. -## Proposed syntax +As a last note, these `where` clauses do not need to involve HRTB, but can +instead apply type/lifetime parameters or concrete types/lifetimes that are +in scope to the type constructor, as in: -The syntax proposed in this RFC is very similar to the syntax of associated -types and type aliases. An advantage of this is that users less familiar with -the intimate details of kindedness will hopefully find this feature intuitive. +```rust +trait Foo { + type Bar where Self::Bar: Display; + type Baz<'a> where Self::Baz<'static>: Send; +} +``` -To declare an associated type operator, simply declare an associated type -with parameters on the type name, as in: +### Assigning an associated type constructor + +Assigning associated type constructors in impls is very similar to the syntax +for assigning associated types: ```rust -trait StreamingIterator { - type Item<'a>; +impl StreamingIterator for StreamIterMut { + type Item<'a> = &'a mut [T]; ... } ``` -Here `Item` is an associated type operator of the kind `& -> *`. +Note that this example makes use of partial application (see the later section +on partial application for more information about this feature). The parameter +to this argument is quite clear, because it is the argument associated with +the type constructor. If there were multiple lifetimes involved, it would still +be unambiguous which was being applied and which isn't, for example: -To apply the associated type operator, simply use it in the position where -a normal type operator would be used instead, as in: +```rust +impl<'a> StreamingIterator for FooStreamIter<'a> { + type Item<'b> = &'b mut [Foo<'a>]; +} +``` + +### Using an associated type constructor to construct a type + +Once a trait has an associated type constructor, it can be applied to any +type/lifetime parameters or concrete types/lifetimes that are in scope. This +can be done both inside the body of the trait and outside of it, using syntax +which is analogous to the syntax for using associated types. Here are some +examples: ```rust trait StreamingIterator { - ... - fn next<'a>(&'a mut self) -> Option>; + type Item<'a>; + // Applying the lifetime parameter `'a` to `Self::Item` inside the trait. + fn next<'a>(&'a self) -> Option>; +} + +struct Foo { + // Applying a concrete lifetime to the constructor outside the trait. + bar: ::Item<'static>; } ``` -To assign the associated type operator, use the parameters in the type -declaration on the right-hand side of the type expression, as in: +Associated type constructors can also be used to construct other type +constructors through partial application (see the later section on partial +application for more information about this feature). ```rust -impl StreamingIterator for StreamIter { - type Item<'a> = &'a [T]; - ... +trait Foo { + type Bar<'a, T>; +} + +trait Baz { + type Quux<'a>; +} + +impl Baz for T where T: Foo { + type Quux<'a> = ::Bar<'a, usize>; +} +``` + +Lastly, lifetimes can be elided in associated type constructors in the same +manner that they can be elided in other type constructors. Considering lifetime +ellision, the full definition of `StreamingIterator` is: + +```rust +trait StreamingIterator { + type Item<'a>; + fn next(&mut self) -> Option; } ``` -Note here that a slice reference has the kind `&, * -> *`, but the local type -parameter `T` is applied to it through partial application to form a type -operator `& -> *`. The syntax makes it clear that the unapplied parameter is -the lifetime `'a`, because `'a` is introduced on the type Item. +### Using associated type constructors in bounds + +Users can bound parameters by the type constructed by that trait's associated +type constructor of a trait using HRTB. Both type equality bounds and trait +bounds of this kind are valid: + +```rust +fn foo StreamingIterator=&'a [i32]>>(iter: T) { ... } + +fn foo(iter: T) where T: StreamingIterator, for<'a> T::Item<'a>: Display { ... } +``` + +See the section on extending HRTBs for more information about that aspect of +this feature. + +This RFC does not propose allowing any sort of bound by the type constructor +itself, whether an equality bound or a trait bound (trait bounds of course are +also impossible). That is, one can do the former but not the latter here: + +```rust +// Valid +fn foo Foo=Vec>>(x: T) { ... } + +// Invalid +fn foo>(x: T) { ... } +``` + +HRTBs allow us to express the same bounds without adding quite as radical a +new feature as adding bounds by equality of type constructors. + +## Partial Application + +In order for this feature to be useful, we will have to allow for type +constructors to partially applied. Many languages with higher-kinded +polymorphism use currying as an alternative to partial application. Rust does +not have currying at the level of expressions, and currying would not be +sufficient to enable the use cases that exist for type constructors, so this +RFC does not propose using currying for higher-kinded polymorphism. + +As an example, the reference operator has the kind `lifetime, type -> type`, +taking both a lifetime and a type to produce a new type. With currying, the two +parameters to the reference operator would have to have a defined order, and it +would be possible to partially apply only one of the parameters to the +reference operator. That is, if it were `lifetime -> type -> type`, one could +apply it to a lifetime to produce a `type -> type` operator, but one could not +apply it to a type to produce a `lifetime -> type` operator. If it were defined +as `type -> lifetime -> type`, it would be restricted in the opposite way. +Because Rust makes use of two base kinds, currying would severely restrict the +forms of abstraction enabled by Rust. + +Instead, when defining an associated type constructor, an anonymous type +constructor can be constructed from a type constructor with more parameters by +applying any of the parameters to that type constructor. The syntax discussed +below makes it unambiguous and easy to see which parameters remain undetermined +at the point of assigning the associated type constructor to a concrete type +constructor. + +When used in a type position, of course, all of the parameters to an associated +type constructor must have been applied to concrete types or type parameters that +are in scope, so that it can be evaluated to a proper type. -This has the same appearance as the declaration of type operator aliases which -are not associated with the trait. +## Extending HRTBs -To add a concrete bound as an associated type operator, the syntax is the same -as adding a concrete bound of an associated type. Here, any types or lifetimes -which are parameters to the associated type operator are omitted (not elided): +Providing bounds on the types constructed by associated type constructors +requires heavy use of HRTBs, or higher-ranked trait bounds. This exists in Rust +today, but in a limited form, and it is an obscure feature primarily used in +the background to make function traits behave as expected. + +In brief, a higher-ranked trait bound is one in which a type or lifetime +parameter is introduced only for the scope of that trait bound. A classic +example of how this can be useful is in the contrast between these two +functions: ```rust -where T: StreamingIterator +fn foo1(x: T, id: F) -> T where F: Fn(T) -> T { + id(x) +} + +fn foo2(x: T, id: F) -> T where F: for Fn(X) -> X { + id(x) +} + +// Valid (evaluates to 4) +foo1::(2, |x| x + x) + +// Invalid (type error) +foo2::(2, |x| x + x) ``` -`&[u8]` is not an elided form of some `&'a [u8]`, but a type operator of the -kind `& -> *`. +In the second function, we _guarantee_ that the `id` argument is the identity +function (ignoring side effects), because it must be a valid function of `X` +to `X` for _all_ `X`, whereas the first can be specialized to only be a valid +function for the type `T`, in this case `i32`. +Higher-ranked trait bounds have several other use cases. Currently, Rust uses +them to declare that arguments with different lifetimes can be passed to +function types, by requiring that that function be valid for all lifetimes, +rather than just for some single lifetime parameter. -However, life time parameters can be elided when applied to associated type -operators in the type position just as they can be elided for concrete type -operators, as in this case, providing a full definition of `StreamingIterator`: +In order to bound associated type constructors, we use higher-ranked types to +require that the type constructor constructs type which meet some bound. This +can be done both in the declaration and when bounding a type parameter by a +trait, and can be both a trait bound and a type equality bound. Here are +examples in code, with their meanings written out: ```rust -trait StreamingIterator { - type Item<'a>; - fn next(&mut self) -> Option; +trait Sequence { + // For every lifetime, this constructor applied to that lifetime must + // produce a type which is an iterator of references of that lifetime + type Iter<'a> where for<'a> Iter<'a>: Iterator; +} + +// For every lifetime, the associated type constructor Item applied to +// that lifetime produces a reference of that lifetime to a slice of bytes. +struct Foo where T: for<'a> StreamingIterator { + ... } ``` +Enabling this requires extending HRTBs to support type parameters as well as +lifetime parameters. This would also imply that HRTBs could introduce type +parameters that themselves have bounds. The syntax for this is left to another +RFC. + +## Benefits of implementing only this feature before other higher-kinded polymorphisms + +This feature is not full-blown higher-kinded polymorphism, and does not allow +for the forms of abstraction that are so popular in Haskell, but it does +provide most of the unique-to-Rust use cases for higher-kinded polymorphism, +such as streaming iterators and collection traits. It is probably also the +most accessible feature for most users, being somewhat easy to understand +intuitively without understanding higher-kindedness. + +This feature has several tricky implementation challenges, but avoids all of +these features that other kinds of higher-kinded polymorphism require: + +* Defining higher-kinded traits +* Implementing higher-kinded traits for type operators +* Higher order type operators +* Type operator parameters bound by higher-kinded traits +* Type operator parameters applied to a given type or type parameter + +## Advantages of proposed syntax + +The advantage of the proposed syntax is that it leverages syntax that already +exists. Type constructors can already be aliased in Rust using the same syntax +that this used, and while type aliases play no polymorphic role in type +resolution, to users they seem very similar to associated types. A goal of this +syntax is that many users will be able to use types which have assocaited type +constructors without even being aware that this has something to do with a type +system feature called higher-kindedness. # Drawbacks [drawbacks]: #drawbacks -## Drawbacks to the concept +## Adding language complexity + +This would add a somewhat complex feature to the language, being able to +polymorphically resolve type constructors, and requires several extensions to +the type system which make the implementation more complicated. + +Additionally, though the syntax is designed to make this feature easy to learn, +it also makes it more plausible that a user may accidentally use it when they +mean something else, similar to the confusion between `impl .. for Trait` and +`impl .. for T where T: Trait`. For example: + +```rust +// The user means this +trait Foo<'a> { + type Bar: 'a; +} + +// But they write this +trait Foo<'a> { + type Bar<'a>; +} +``` + +## Not full "higher-kinded types" + +This does not add all of the features people want when they talk about higher- +kinded types. For example, it does not enable traits like `Monad`. Some people +may prefer to implement all of these features together at once. However, this +feature is forward compatible with other kinds of higher-kinded polymorphism, +and doesn't preclude implementing them in any way. In fact, it paves the way +by solving some implementation details that will impact other kinds of higher- +kindedness as well, such as partial application. -This adds complexity to the language, and implements a part of higher-kinded -polymorphism without all of the benefits that come along with it. There are -valid arguments in favor of waiting until additional forms of higher-kinded -polymorphism have been worked out, as well as in favor of never implementing -higher-kinded polymorphism at all. +## Syntax isn't like other forms of higher-kinded polymorphism -## Drawbacks to the syntax +Though the proposed syntax is very similar to the syntax for associated types +and type aliases, it is probably not possible for other forms of higher-kinded +polymorphism to use a syntax along the same lines. For this reason, the syntax +used to define an associated type constructor will probably be very different +from the syntax used to e.g. implement a trait for a type constructor. -Though this syntax is a natural fit for associated type operators, it is not -a natural syntax for other forms of higher-kinded polymorphism. As a result, -the syntaxes of two related forms of polymorphism will be significantly -different. We believe this cost is justified by the advantages of making the -syntax similar to associated types. +However, the syntax used for these other forms of higher-kinded polymorphism +will depend on exactly what features they enable. It would be hard to design +a syntax which is consistent with unknown features. # Alternatives [alternatives]: #alternatives -An alternative is to push harder on higher-ranked lifetimes, possibly -introducing some elision that would make them easier to use. +## Push HRTBs harder without associated type constructors + +An alternative is to push harder on HRTBs, possibly introducing some elision +that would make them easier to use. Currently, an approximation of `StreamingIterator` can be defined like this: @@ -278,9 +453,19 @@ You can then bound types as `T: for<'a> StreamingIterator<'a>` to avoid the lifetime parameter infecting everything `StreamingIterator` appears. However, this only partially prevents the infectiveness of `StreamingIterator`, -only allows for some of the types that associated type operators can express, -and is in generally a hacky attempt to work around the limitation rather than -an equivalent alternative. +only allows for some of the types that associated type constructors can +express, and is in generally a hacky attempt to work around the limitation +rather than an equivalent alternative. + +## Only add associated type constructors whose arguments are lifetimes + +If associated type constructors could only take lifetime arguments, much of the +work extending HRTBs would not be necessary. Associated type constructors with +lifetime parameters only covers the primary known use cases for this feature. +Though it is inelegant to treat lifetime parameters differently from type +parameters here, at least as an implementation strategy it may make sense to +first implement this feature with lifetime parameters, and later extend it to +type parameters as well. # Unresolved questions [unresolved]: #unresolved-questions