mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-10-05 22:02:55 +01:00
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:
committed by
GitHub
co-authored by
Richard Smith
parent
62c8821f20
commit
b9538129a2
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user