mirror of https://github.com/rust-lang/rfcs.git
Generic associated types (associated type constructors) (#1598)
RFC 1598 is Generic Associated Types.
This commit is contained in:
parent
46267a49ad
commit
a7cd91048e
|
@ -0,0 +1,480 @@
|
|||
- Feature Name: generic_associated_types
|
||||
- Start Date: 2016-04-29
|
||||
- RFC PR: [rust-lang/rfcs#1598](https://github.com/rust-lang/rfcs/pull/1598)
|
||||
- Rust Issue: [rust-lang/rust#44265](https://github.com/rust-lang/rust/issues/44265)
|
||||
|
||||
# Summary
|
||||
[summary]: #summary
|
||||
|
||||
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
|
||||
[motivation]: #motivation
|
||||
|
||||
Consider the following trait as a representative motivating example:
|
||||
|
||||
```rust
|
||||
trait StreamingIterator {
|
||||
type Item<'a>;
|
||||
fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
|
||||
}
|
||||
```
|
||||
|
||||
This trait is very useful - it allows for a kind of Iterator which yields
|
||||
values which have a lifetime tied to the lifetime of the reference passed to
|
||||
`next`. A particular obvious use case for this trait would be an iterator over
|
||||
a vector which yields overlapping, mutable subslices with each iteration. Using
|
||||
the standard `Iterator` interface, such an implementation would be invalid,
|
||||
because each slice would be required to exist for as long as the iterator,
|
||||
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 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
|
||||
|
||||
## Background: What is kindedness?
|
||||
|
||||
"Higher-kinded types" is a vague term, conflating multiple language features
|
||||
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 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 {
|
||||
!x
|
||||
}
|
||||
```
|
||||
|
||||
`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 to understanding higher-kindedness.
|
||||
|
||||
In the analysis of kinds, all of these types - `bool`, `char`, `bool -> bool`
|
||||
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<i32>` (which is
|
||||
`type`) is analogous to the difference between the type of `not` and
|
||||
`not(true)`. Note that `Vec<T>` has the kind `type`, just like `Vec<i32>`: 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
|
||||
`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`.
|
||||
|
||||
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`.
|
||||
|
||||
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.
|
||||
|
||||
## Features of associated type constructors
|
||||
|
||||
### Declaring & assigning an associated type constructor
|
||||
|
||||
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
|
||||
trait StreamingIterator {
|
||||
type Item<'a>;
|
||||
}
|
||||
```
|
||||
|
||||
It is clear that the `Item` associated item is a type constructor, rather than
|
||||
a type, because it has a type parameter attached to it.
|
||||
|
||||
Associated type constructors can be bounded, just like associated types can be:
|
||||
|
||||
```rust
|
||||
trait Iterable {
|
||||
type Item<'a>;
|
||||
type Iter<'a>: Iterator<Item = Self::Item<'a>>;
|
||||
|
||||
fn iter<'a>(&'a self) -> Self::Iter<'a>;
|
||||
}
|
||||
```
|
||||
|
||||
This bound is applied to the "output" of the type constructor, and the parameter
|
||||
is treated as a higher rank parameter. That is, the above bound is roughly
|
||||
equivalent to adding this bound to the trait:
|
||||
|
||||
```rust
|
||||
for<'a> Self::Iter<'a>: Iterator<Item = Self::Item<'a>>
|
||||
```
|
||||
|
||||
Assigning associated type constructors in impls is very similar to the syntax
|
||||
for assigning associated types:
|
||||
|
||||
```rust
|
||||
impl<T> StreamingIterator for StreamIterMut<T> {
|
||||
type Item<'a> = &'a mut [T];
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Using an associated type constructor to construct a type
|
||||
|
||||
Once a trait has an associated type constructor, it can be applied to any
|
||||
parameters or concrete term 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 {
|
||||
type Item<'a>;
|
||||
// Applying the lifetime parameter `'a` to `Self::Item` inside the trait.
|
||||
fn next<'a>(&'a self) -> Option<Self::Item<'a>>;
|
||||
}
|
||||
|
||||
struct Foo<T: StreamingIterator> {
|
||||
// Applying a concrete lifetime to the constructor outside the trait.
|
||||
bar: <T as StreamingIterator>::Item<'static>;
|
||||
}
|
||||
```
|
||||
|
||||
Associated type constructors can also be used to construct other type
|
||||
constructors:
|
||||
|
||||
```rust
|
||||
trait Foo {
|
||||
type Bar<'a, 'b>;
|
||||
}
|
||||
|
||||
trait Baz {
|
||||
type Quux<'a>;
|
||||
}
|
||||
|
||||
impl<T> Baz for T where T: Foo {
|
||||
type Quux<'a> = <T as Foo>::Bar<'a, 'static>;
|
||||
}
|
||||
```
|
||||
|
||||
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<Self::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<T: for<'a> StreamingIterator<Item<'a>=&'a [i32]>>(iter: T) { ... }
|
||||
|
||||
fn foo<T>(iter: T) where T: StreamingIterator, for<'a> T::Item<'a>: Display { ... }
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
## Associated type constructors of type arguments
|
||||
|
||||
All of the examples in this RFC have focused on associated type constructors of
|
||||
lifetime arguments, however, this RFC proposes adding ATCs of types as well:
|
||||
|
||||
```rust
|
||||
trait Foo {
|
||||
type Bar<T>;
|
||||
}
|
||||
```
|
||||
|
||||
This RFC does **not** propose extending HRTBs to take type arguments, which
|
||||
makes these less expressive than they could be. Such an extension is desired,
|
||||
but out of scope for this RFC.
|
||||
|
||||
Type arguments can be used to encode other forms of higher kinded polymorphism
|
||||
using the "family" pattern. For example, Using the `PointerFamily` trait, you
|
||||
can abstract over Arc and Rc:
|
||||
|
||||
```rust
|
||||
trait PointerFamily {
|
||||
type Pointer<T>: Deref<Target = T>;
|
||||
fn new<T>(value: T) -> Self::Pointer<T>;
|
||||
}
|
||||
|
||||
struct ArcFamily;
|
||||
|
||||
impl PointerFamily for ArcFamily {
|
||||
type Pointer<T> = Arc<T>;
|
||||
fn new<T>(value: T) -> Self::Pointer<T> {
|
||||
Arc::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
struct RcFamily;
|
||||
|
||||
impl PointerFamily for RcFamily {
|
||||
type Pointer<T> = Rc<T>;
|
||||
fn new<T>(value: T) -> Self::Pointer<T> {
|
||||
Rc::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
struct Foo<P: PointerFamily> {
|
||||
bar: P::Pointer<String>,
|
||||
}
|
||||
```
|
||||
|
||||
## Evaluating bounds and where clauses
|
||||
|
||||
### Bounds on associated type constructors
|
||||
|
||||
Bounds on associated type constructors are treated as higher rank bounds on the
|
||||
trait itself. This makes their behavior consistent with the behavior of bounds
|
||||
on regular associated types. For example:
|
||||
|
||||
```rust
|
||||
trait Foo {
|
||||
type Assoc<'a>: Trait<'a>;
|
||||
}
|
||||
```
|
||||
|
||||
Is equivalent to:
|
||||
|
||||
```rust
|
||||
trait Foo where for<'a> Self::Assoc<'a>: Trait<'a> {
|
||||
type Assoc<'a>;
|
||||
}
|
||||
```
|
||||
|
||||
### `where` clauses on associated types
|
||||
|
||||
In contrast, where clauses on associated types introduce constraints which must
|
||||
be proven each time the associated type is used. For example:
|
||||
|
||||
```rust
|
||||
trait Foo {
|
||||
type Assoc where Self: Sized;
|
||||
}
|
||||
```
|
||||
|
||||
Each invokation of `<T as Foo>::Assoc` will need to prove `T: Sized`, as
|
||||
opposed to the impl needing to prove the bound as in other cases.
|
||||
|
||||
(@nikomatsakis believes that where clauses will be needed on associated type
|
||||
constructors specifically to handle lifetime well formedness in some cases.
|
||||
The exact details are left out of this RFC because they will emerge more fully
|
||||
during implementation.)
|
||||
|
||||
## 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.
|
||||
|
||||
# How We Teach This
|
||||
[how-we-teach-this]: #how-we-teach-this
|
||||
|
||||
This RFC uses the terminology "associated type constructor," which has become
|
||||
the standard way to talk about this feature in the Rust community. This is not
|
||||
a very accessible framing of this concept; in particular the term "type
|
||||
constructor" is an obscure piece of jargon from type theory which most users
|
||||
cannot be expected to be familiar with.
|
||||
|
||||
Upon accepting this RFC, we should begin (with haste) refering to this concept
|
||||
as simply "generic associated types." Today, associated types cannot be
|
||||
generic; after this RFC, this will be possible. Rather than teaching this as
|
||||
a separate feature, it will be taught as an advanced use case for associated
|
||||
types.
|
||||
|
||||
Patterns like "family traits" should also be taught in some way, possible in
|
||||
the book or possibly just through supplemental forms of documentation like
|
||||
blog posts.
|
||||
|
||||
This will also likely increase the frequency with which users have to employ
|
||||
higher rank trait bounds; we will want to put additional effort into teaching
|
||||
and making teachable HRTBs.
|
||||
|
||||
# Drawbacks
|
||||
[drawbacks]: #drawbacks
|
||||
|
||||
## 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<T> .. 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.
|
||||
|
||||
## Syntax isn't like other forms of higher-kinded polymorphism
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
## 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:
|
||||
|
||||
```rust
|
||||
trait StreamingIterator<'a> {
|
||||
type Item: 'a;
|
||||
fn next(&'a self) -> Option<Self::Item>;
|
||||
}
|
||||
```
|
||||
|
||||
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 constructors can
|
||||
express, and is in generally a hacky attempt to work around the limitation
|
||||
rather than an equivalent alternative.
|
||||
|
||||
## Impose restrictions on ATCs
|
||||
|
||||
What is often called "full higher kinded polymorphism" is allowing the use of
|
||||
type constructors as input parameters to other type constructors - higher order
|
||||
type constructors, in other words. Without any restrictions, multiparameter
|
||||
higher order type constructors present serious problems for type inference.
|
||||
|
||||
For example, if you are attempting to infer types, and you know you have a
|
||||
constructor of the form `type, type -> Result<(), io::Error>`, without any
|
||||
restrictions it is difficult to determine if this constructor is
|
||||
`(), io::Error -> Result<(), io::Error>` or `io::Error, () -> Result<(), io::Error>`.
|
||||
|
||||
Because of this, languages with first class higher kinded polymorphism tend to
|
||||
impose restrictions on these higher kinded terms, such as Haskell's currying
|
||||
rules.
|
||||
|
||||
If Rust were to adopt higher order type constructors, it would need to impose
|
||||
similar restrictions on the kinds of type constructors they can receive. But
|
||||
associated type constructors, being a kind of alias, inherently mask the actual
|
||||
structure of the concrete type constructor. In other words, if we want to be
|
||||
able to use ATCs as arguments to higher order type constructors, we would need
|
||||
to impose those restrictions on *all* ATCs.
|
||||
|
||||
We have a list of restrictions we believe are necessary and sufficient; more
|
||||
background can be found in [this blog post](http://smallcultfollowing.com/babysteps/blog/2016/11/09/associated-type-constructors-part-4-unifying-atc-and-hkt/)
|
||||
by nmatsakis:
|
||||
|
||||
* Each argument to the ATC must be applied
|
||||
* They must be applied in the same order they appear in the ATC
|
||||
* They must be applied exactly once
|
||||
* They must be the left-most arguments of the constructor
|
||||
|
||||
These restrictions are quite constrictive; there are several applications of
|
||||
ATCs that we already know about that would be frustrated by this, such as the
|
||||
definition of `Iterable` for `HashMap` (for which the item `(&'a K, &'a V)`,
|
||||
applying the lifetime twice).
|
||||
|
||||
For this reason we have decided **not** to apply these restrictions to all
|
||||
ATCs. This will mean that if higher order type constructors are ever added to
|
||||
the language, they will not be able to take an abstract ATC as an argument.
|
||||
However, this can be maneuvered around using newtypes which do meet the
|
||||
restrictions, for example:
|
||||
|
||||
```rust
|
||||
struct IterItem<'a, I: Iterable>(I::Item<'a>);
|
||||
```
|
||||
|
||||
# Unresolved questions
|
||||
[unresolved]: #unresolved-questions
|
Loading…
Reference in New Issue