Generic details 12: parameterized types (#1146)

This proposal has three main contributions:
- Types with generic parameters have an identity that consists of the types names plus the values of those parameters.
- The parameters of a type may be deduced from a function's argument.
- Types with generic parameters do not support specialization. Instead, a type can delegate to an interface to opt in to allowing specific customization points.

Co-authored-by: Richard Smith <richard@metafoo.co.uk>
This commit is contained in:
josh11b
2022-04-28 16:16:00 -07:00
committed by GitHub
co-authored by Richard Smith
parent 62c8821f20
commit b9538129a2
2 changed files with 251 additions and 6 deletions
+166 -6
View File
@@ -106,6 +106,8 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
- [Operator overloading](#operator-overloading)
- [Binary operators](#binary-operators)
- [`like` operator for implicit conversions](#like-operator-for-implicit-conversions)
- [Parameterized types](#parameterized-types)
- [Specialization](#specialization)
- [Future work](#future-work)
- [Dynamic types](#dynamic-types)
- [Runtime type parameters](#runtime-type-parameters)
@@ -118,7 +120,6 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
- [Generic associated types](#generic-associated-types)
- [Higher-ranked types](#higher-ranked-types)
- [Field requirements](#field-requirements)
- [Generic type specialization](#generic-type-specialization)
- [Bridge for C++ customization points](#bridge-for-c-customization-points)
- [Variadic arguments](#variadic-arguments)
- [Range constraints on generic integers](#range-constraints-on-generic-integers)
@@ -5154,6 +5155,169 @@ external impl [T:! IntLike] like T
as MultipliableWith(like T) where .Result = T;
```
## Parameterized types
Types may have generic parameters. Those parameters may be used to specify types
in the declarations of its members, such as data fields, member functions, and
even interfaces being implemented. For example, a container type might be
parameterized by the type of its elements:
```
class HashMap(
KeyType:! Hashable & EqualityComparable & Movable,
ValueType:! Movable) {
// `Self` is `HashMap(KeyType, ValueType)`.
// Parameters may be used in function signatures.
fn Insert[addr me: Self*](k: KeyType, v: ValueType);
// Parameters may be used in field types.
private var buckets: Vector((KeyType, ValueType));
// Parameters may be used in interfaces implemented.
impl as Container where .ElementType = (KeyType, ValueType);
impl as ComparableWith(HashMap(KeyType, ValueType));
}
```
Note that, unlike functions, every parameter to a type must either be generic or
template, using `:!` or `template...:!`, not dynamic, with a plain `:`.
Two types are the same if they have the same name and the same arguments.
Carbon's [manual type equality](#manual-type-equality) approach means that the
compiler may not always be able to tell when two type expressions are equal
without help from the user, in the form of
[`observe` declarations](#observe-declarations). This means Carbon will not in
general be able to determine when types are unequal.
Unlike an [interface's parameters](#parameterized-interfaces), a type's
parameters may be [deduced](terminology.md#deduced-parameter), as in:
```
fn ContainsKey[KeyType:! Movable, ValueType:! Movable]
(haystack: HashMap(KeyType, ValueType), needle: KeyType)
-> bool { ... }
fn MyMapContains(s: String) {
var map: HashMap(String, i32) = (("foo", 3), ("bar", 5));
// ✅ Deduces `KeyType` = `String` from the types of both arguments.
// Deduces `ValueType` = `i32` from the type of the first argument.
return ContainsKey(map, s);
}
```
Note that restrictions on the type's parameters from the type's declaration can
be [implied constraints](#implied-constraints) on the function's parameters.
### Specialization
[Specialization](terminology.md#generic-specialization) is used to improve
performance in specific cases when a general strategy would be inefficient. For
example, you might use
[binary search](https://en.wikipedia.org/wiki/Binary_search_algorithm) for
containers that support random access and keep their contents in sorted order
but [linear search](https://en.wikipedia.org/wiki/Linear_search) in other cases.
Types, like functions, may not be specialized directly in Carbon. This effect
can be achieved, however, through delegation.
For example, imagine we have a parameterized class `Optional(T)` that has a
default storage strategy that works for all `T`, but for some types we have a
more efficient approach. For pointers we can use a
[null value](https://en.wikipedia.org/wiki/Null_pointer) to represent "no
pointer", and for booleans we can support `True`, `False`, and `None` in a
single byte. Clients of the optional library may want to add additional
specializations for their own types. We make an interface that represents "the
storage of `Optional(T)` for type `T`," written here as `OptionalStorage`:
```
interface OptionalStorage {
let Storage:! Type;
fn MakeNone() -> Storage;
fn Make(x: Self) -> Storage;
fn IsNone(x: Storage) -> bool;
fn Unwrap(x: Storage) -> Self;
}
```
The default implementation of this interface is provided by a
[blanket implementation](#blanket-impls):
```
// Default blanket implementation
impl [T:! Movable] T as OptionalStorage
where .Storage = (bool, T) {
...
}
```
This implementation can then be
[specialized](#lookup-resolution-and-specialization) for more specific type
patterns:
```
// Specialization for pointers, using nullptr == None
final external impl [T:! Type] T* as OptionalStorage
where .Storage = Array(Byte, sizeof(T*)) {
...
}
// Specialization for type `bool`.
final external impl bool as OptionalStorage
where .Storage = Byte {
...
}
```
Further, libraries can implement `OptionalStorage` for their own types, assuming
the interface is not marked `private`. Then the implementation of `Optional(T)`
can delegate to `OptionalStorage` for anything that can vary with `T`:
```
class Optional(T:! Movable) {
fn None() -> Self {
return {.storage = T.(OptionalStorage.MakeNone)()};
}
fn Some(x: T) -> Self {
return {.storage = T.(OptionalStorage.Make)(x)};
}
...
private var storage: T.(OptionalStorage.Storage);
}
```
Note that the constraint on `T` is just `Movable`, not
`Movable & OptionalStorage`, since the `Movable` requirement is
[sufficient to guarantee](#lookup-resolution-and-specialization) that some
implementation of `OptionalStorage` exists for `T`. Carbon does not require
callers of `Optional`, even generic callers, to specify that the argument type
implements `OptionalStorage`:
```
// ✅ Allowed: `T` just needs to be `Movable` to form `Optional(T)`.
// A `T:! OptionalStorage` constraint is not required.
fn First[T:! Movable & Eq](v: Vector(T)) -> Optional(T);
```
Adding `OptionalStorage` to the constraints on the parameter to `Optional` would
obscure what types can be used as arguments. `OptionalStorage` is an
implementation detail of `Optional` and need not appear in its public API.
In this example, a `let` is used to avoid repeating `OptionalStorage` in the
definition of `Optional`, since it has no name conflicts with the members of
`Movable`:
```
class Optional(T:! Movable) {
private let U:! Movable & OptionalStorage = T;
fn None() -> Self {
return {.storage = U.MakeNone()};
}
fn Some(x: T) -> Self {
return {.storage = u.Make(x)};
}
...
private var storage: U.Storage;
}
```
## Future work
### Dynamic types
@@ -5230,11 +5394,6 @@ implementing type has a particular field. This would be to match the
expressivity of inheritance, which can express "all subtypes start with this
list of fields."
### Generic type specialization
See [generic specialization](terminology.md#generic-specialization) for a
description of what this might involve.
### Bridge for C++ customization points
See details in [the goals document](goals.md#bridge-for-c-customization-points).
@@ -5273,3 +5432,4 @@ parameter, as opposed to an associated type, as in `N:! u32 where ___ >= 2`.
- [#1013: Generics: Set associated constants using `where` constraints](https://github.com/carbon-language/carbon-lang/pull/1013)
- [#1084: Generics details 9: forward declarations](https://github.com/carbon-language/carbon-lang/pull/1084)
- [#1144: Generic details 11: operator overloading](https://github.com/carbon-language/carbon-lang/pull/1144)
- [#1146: Generic details 12: parameterized types](https://github.com/carbon-language/carbon-lang/pull/1146)
+85
View File
@@ -0,0 +1,85 @@
# Generic details 12: parameterized types
<!--
Part of the Carbon Language project, under the Apache License v2.0 with LLVM
Exceptions. See /LICENSE for license information.
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-->
[Pull request](https://github.com/carbon-language/carbon-lang/pull/1146)
<!-- toc -->
## Table of contents
- [Problem](#problem)
- [Background](#background)
- [Proposal](#proposal)
- [Rationale based on Carbon's goals](#rationale-based-on-carbons-goals)
- [Alternatives considered](#alternatives-considered)
<!-- tocstop -->
## Problem
Most aspects of generic parameterization are the same between functions and
types, but there are a few things specific to types. In particular:
- the declaration of a type can contain a greater variety of members, and
- types have identity which affects type comparisons, deduced parameters, and
implied constraints.
We also want a
[generic specialization](/docs/design/generics/terminology.md#generic-specialization)
story that works well for types, without giving up the ability to type check
users of a type without knowing which specializations apply.
## Background
C++ supports specialization, including partial specialization, for templated
types and functions.
## Proposal
This proposal adds a
["parameterized types" section](/docs/design/generics/details.md#parameterized-types)
to the [detailed design of generics](/docs/design/generics/details.md). Of note,
it proposes not to support specialization of types or functions since those use
cases can be handled by delegating to interfaces, which already support
specialization.
## Rationale based on Carbon's goals
Specialization is important for allowing code to be generic without sacrificing
[performance](/docs/project/goals.md#performance-critical-software). Since there
is already a way to support specialization use cases without adding direct
support for specializing types, this proposal follows the Carbon principle to
[prefer providing only one way to do a given thing](/docs/project/principles/one_way.md).
By avoiding another way of customizing behavior for specific types, it makes
interfaces the
[single static open extension mechanism](/docs/project/principles/static_open_extension.md).
This proposal maintains consistency between generic parameterization of types
and functions, in support of
[code that is easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write).
## Alternatives considered
We considered supporting specialization for types directly. To support type
checking in a generic context, the API of the type needs to be defined
independent of which specialization is selected. This would have introduced
complexity into the language:
- Would the API of the type be represented by an `interface`?
- Would the type's API be explicitly declared or inferred from the type
declaration by some process, at the risk of including details that are not
necessarily stable?
- How would public data members be handled, since interfaces (currently) don't
support them?
- How would we support non-monomorphizing generic strategies? With the current
proposal, the layout of a parameterized type is known unless it uses an
associated type.
The main disadvantage of the proposed approach is that the author of the type
needs to define the ways that the type can be customized. We will need to see if
this ends up being a problem in practice. It may turn out to be a benefit, by
giving more information about the implementation of a class to readers.