diff --git a/docs/design/generics/details.md b/docs/design/generics/details.md index c603f121eee21..62107ddefd1ab 100644 --- a/docs/design/generics/details.md +++ b/docs/design/generics/details.md @@ -98,6 +98,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) @@ -110,7 +112,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) @@ -4785,6 +4786,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 @@ -4861,11 +5025,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). @@ -4911,3 +5070,4 @@ be included in the declaration as well. - [#990: Generics details 8: interface default and final members](https://github.com/carbon-language/carbon-lang/pull/990) - [#1013: Generics: Set associated constants using where constraints](https://github.com/carbon-language/carbon-lang/pull/1013) - [#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) diff --git a/proposals/p1146.md b/proposals/p1146.md new file mode 100644 index 0000000000000..a97e92162ce44 --- /dev/null +++ b/proposals/p1146.md @@ -0,0 +1,85 @@ +# Generic details 12: parameterized types + + + +[Pull request](https://github.com/carbon-language/carbon-lang/pull/1146) + + + +## Table of contents + +- [Problem](#problem) +- [Background](#background) +- [Proposal](#proposal) +- [Rationale based on Carbon's goals](#rationale-based-on-carbons-goals) +- [Alternatives considered](#alternatives-considered) + + + +## 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.