Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic details 12: parameterized types #1146

Merged
merged 14 commits into from
Apr 28, 2022
172 changes: 166 additions & 6 deletions docs/design/generics/details.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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 { ... }
josh11b marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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)
85 changes: 85 additions & 0 deletions proposals/p1146.md
Original file line number Diff line number Diff line change
@@ -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?
zygoloid marked this conversation as resolved.
Show resolved Hide resolved
- 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.