Move adapters from generics to class design docs (#7561)

Assisted-by: Gemini via Antigravity

---------

Co-authored-by: Josh L <josh11b@users.noreply.github.com>
This commit is contained in:
josh11b
2026-07-27 22:33:37 +00:00
committed by GitHub
co-authored by Josh L
parent f38085cbca
commit 1cef214e6a
8 changed files with 132 additions and 133 deletions
+106 -2
View File
@@ -57,6 +57,8 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
- [Partial class type](#partial-class-type)
- [Usage](#usage)
- [Assignment with inheritance](#assignment-with-inheritance)
- [Compatible types](#compatible-types)
- [Adapters](#adapters)
- [Destructors](#destructors)
- [Access control](#access-control)
- [Private access](#private-access)
@@ -1648,6 +1650,102 @@ implement it for final types. However, following the
we allow users to also implement assignment on extensible classes, even though
it can lead to [slicing](https://en.wikipedia.org/wiki/Object_slicing).
### Compatible types
Two types are compatible if they have the same notional set of values and
represent those values in the same way, even if they expose different APIs. The
representation of a type describes how the values of that type are represented
as a sequence of bits in memory. The set of values of a type includes properties
that the compiler can't directly see, such as invariants that the type
maintains.
We can't just say two types are compatible based on structural reasons. Instead,
we have specific constructs that create compatible types from existing types in
ways that encourage preserving the programmer's intended semantics and
invariants, such as implementing the API of the new type by calling (public)
methods of the original API, instead of accessing any private implementation
details.
Casting a value between compatible types is safe without any dynamic checks or
danger of [object slicing](https://en.wikipedia.org/wiki/Object_slicing).
#### Adapters
An adapter creates a new type compatible with an existing type, but with a
different API. Adapters are defined by using the `adapt` keyword inside a
`class` definition:
```carbon
class Song {
fn Title(self) -> String;
}
class SongByTitle {
adapt Song;
}
```
The rules for adapters are:
- You can add any declaration that you could add to a class except for
declarations that would change the representation of the type. This means
you can add methods, functions, interface implementations, and aliases, but
not fields, base classes, or virtual functions. The specific implementations
of virtual functions are part of the type representation, and so no virtual
functions may be overridden in an adapter either.
- The adapted type is compatible with the original type, and that relationship
is an equivalence class, so `Song`, `SongByTitle`, and any other adapters of
`Song` end up compatible with each other.
- Since adapted types are compatible with the original type, you may
explicitly cast between them, but there is no implicit conversion between
these types.
Inside an adapter, the `Self` type matches the adapter. Members of the original
type may be accessed by a cast:
```carbon
class SongByTitle {
adapt Song;
fn Less(self, rhs: Self) -> bool {
return (self as Song).Title() < (rhs as Song).Title();
}
}
```
An adapter can also preserve the API and interface implementations of the original
type using `extend adapt`. For details on how an extending adapter implements
interfaces that are implemented for the adapted type, as well as applications of adapters to generics, see
[Adapting types](/docs/design/generics/details.md#adapting-types) in the generics
design.
**Comparison with other languages:** This is similar to the Rust idiom called
"newtype", which is used to implement traits on types while avoiding
[coherence](/docs/design/generics/terminology.md#coherence) problems, see
[here](https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#using-the-newtype-pattern-to-implement-external-traits-on-external-types)
and
[here](https://github.com/Ixrec/rust-orphan-rules#user-content-why-are-the-orphan-rules-controversial).
Rust's mechanism doesn't directly support reusing implementations, though some
of that is provided by macros defined in libraries.
Rust also uses the newtype idiom to create types with additional invariants or
other information encoded in the type
([1](https://doc.rust-lang.org/rust-by-example/generics/new_types.html),
[2](https://doc.rust-lang.org/book/ch19-04-advanced-types.html#using-the-newtype-pattern-for-type-safety-and-abstraction),
[3](https://www.worthe-it.co.za/blog/2020-10-31-newtype-pattern-in-rust.html)).
This is used to record in the type system that some data has passed validation
checks, like `ValidDate` with the same data layout as `Date`. Or to record the
units associated with a value, such as `Seconds` versus `Milliseconds` or `Feet`
versus `Meters`.
> **Future work:** We should have some way of restricting the casts between a type
> and an adapter to address this use case. One possibility would be to add the
> keyword `private` before `adapt`, so you might write
> `extend private adapt Date;`.
Haskell has a [`newtype` feature](https://wiki.haskell.org/Newtype) as well.
Haskell's feature doesn't directly support reusing implementations either, but
the most popular compiler provides it as
[an extension](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/newtype_deriving.html).
### Destructors
Every non-abstract type is _destructible_, meaning has a defined destructor
@@ -1765,8 +1863,7 @@ interface Allocator {
To pass a pointer to a base class without a virtual destructor to a
checked-generic function expecting a `Deletable` type, use the
`UnsafeAllowDelete`
[type adapter](/docs/design/generics/details.md#adapting-types).
`UnsafeAllowDelete` [type adapter](#adapters).
```
class UnsafeAllowDelete(T: Concrete) {
@@ -2323,6 +2420,10 @@ the type of `U.x`."
- [Nominal data class](/proposals/p000722-nominal-classes-and-methods.md#nominal-data-class)
- [Let constants](/proposals/p000722-nominal-classes-and-methods.md#let-constants)
- [#731: Generics details 2: adapters, associated types, parameterized interfaces](https://github.com/carbon-language/carbon-lang/pull/731)
- [`adaptor` instead of `adapter`](/proposals/p000731-generics-details-2-adapters-associated-types-parameterized-interfaces.md#adaptor-instead-of-adapter)
- [#777: Inheritance](https://github.com/carbon-language/carbon-lang/pull/777)
- [Classes are final by default](/proposals/p000777-inheritance.md#classes-are-final-by-default)
@@ -2376,6 +2477,8 @@ the type of `U.x`."
- [Use `extends` instead of `extend`](/proposals/p002760-consistent-class-and-interface-syntax.md#use-extends-instead-of-extend)
- [List base class in class declaration](/proposals/p002760-consistent-class-and-interface-syntax.md#list-base-class-in-class-declaration)
- [Continue to use `adapter` or `adaptor` instead of `adapt`](/proposals/p002760-consistent-class-and-interface-syntax.md#continue-to-use-adapter-or-adaptor-instead-of-adapt)
- [Use some other syntax for extending adapters](/proposals/p002760-consistent-class-and-interface-syntax.md#use-some-other-syntax-for-extending-adapters)
- [#5017: Destructor syntax](https://github.com/carbon-language/carbon-lang/pull/5017)
@@ -2401,6 +2504,7 @@ the type of `U.x`."
- [#257: Initialization of memory and variables](https://github.com/carbon-language/carbon-lang/pull/257)
- [#561: Basic classes: use cases, struct literals, struct types, and future work](https://github.com/carbon-language/carbon-lang/pull/561)
- [#722: Nominal classes and methods](https://github.com/carbon-language/carbon-lang/pull/722)
- [#731: Generics details 2: adapters, associated types, parameterized interfaces](https://github.com/carbon-language/carbon-lang/pull/731)
- [#777: Inheritance](https://github.com/carbon-language/carbon-lang/pull/777)
- [#875: Principle: Information accumulation](https://github.com/carbon-language/carbon-lang/pull/875)
- [#981: Implicit conversions for aggregates](https://github.com/carbon-language/carbon-lang/pull/981)
+2 -2
View File
@@ -153,7 +153,7 @@ Lossy conversions between `iN` or `uN` and `iM` or `uM` are not supported with
The following conversion is supported by `as`:
- `T` -> `U` if `T` is
[compatible](../generics/terminology.md#compatible-types) with `U`.
[compatible](/docs/design/classes.md#compatible-types) with `U`.
**Future work:** We may need a mechanism to restrict which conversions between
adapters are permitted and which code can perform them. Some of the conversions
@@ -162,7 +162,7 @@ permitted by this rule may only be allowed in certain contexts.
## Extensibility
Explicit casts can be defined for user-defined types such as
[classes](../classes.md) by implementing the `As` interface:
[classes](/docs/design/classes.md) by implementing the `As` interface:
```
interface As(Dest: type) {
+1 -1
View File
@@ -46,7 +46,7 @@ These last two cases are highlighted as concerns in Rust in
Since Carbon is bundling interface implementations into types, for the
convenience and expressiveness that provides, we satisfy those use cases by
giving the user control over the type of a value. This means having facilities
for defining new [compatible types](terminology.md#compatible-types) with
for defining new [compatible types](/docs/design/classes.md#compatible-types) with
different interface implementations, and casting between those types as needed.
## The "Hashtable Problem"
+13 -79
View File
@@ -45,7 +45,6 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
- [Use case: Defining an impl for use by other types](#use-case-defining-an-impl-for-use-by-other-types)
- [Use case: Private impl](#use-case-private-impl)
- [Use case: Accessing interface names](#use-case-accessing-interface-names)
- [Future work: Adapter with stricter invariants](#future-work-adapter-with-stricter-invariants)
- [Associated constants](#associated-constants)
- [Associated functions](#associated-functions)
- [Associated facets](#associated-facets)
@@ -317,7 +316,7 @@ Assert(p1.Add(p1) == p2);
```
For more on how `extend` affects member access, see
[member access](../expressions/member_access.md#extend).
[member access](/docs/design/expressions/member_access.md#extend).
Without `extend`, those methods may only be accessed with
[qualified member names and compound member access](#qualified-member-names-and-compound-member-access):
@@ -1433,7 +1432,7 @@ fn DoHashAndEquals[T: Hashable](x: T) {
> interface doesn't impl the interfaces it extends, as adopted in
> [#5168: Forward `impl` declaration of an incomplete interface](/proposals/p005168-forward-impl-declaration-of-an-incomplete-interface.md).
> Should link to
> [`extend` in member access](../expressions/member_access.md#extend).
> [`extend` in member access](/docs/design/expressions/member_access.md#extend).
When implementing an interface, we allow implementing the aliased names as well.
In the case of `Hashable` above, this includes all the members of `Equatable`,
@@ -1800,10 +1799,9 @@ be detected in function overloading.
Since interfaces may only be implemented for a type once, and we limit where
implementations may be added to a type, there is a need to allow the user to
switch the type of a value to access different interface implementations. Carbon
therefore provides a way to create new types
[compatible with](terminology.md#compatible-types) existing types with different
APIs, in particular with different interface implementations, by
[adapting](terminology.md#adapting-a-type) them:
therefore provides [adapters](/docs/design/classes.md#adapters) as a way to create new types
[compatible with](/docs/design/classes.md#compatible-types) existing types with different
APIs, in particular with different interface implementations:
```carbon
interface Printable {
@@ -1835,60 +1833,11 @@ class FormattedSongByTitle {
This allows developers to provide implementations of new interfaces (as in
`SongByTitle`), provide different implementations of the same interface (as in
`FormattedSong`), or mix and match implementations from other compatible types
(as in `FormattedSongByTitle`). The rules are:
(as in `FormattedSongByTitle`).
- You can add any declaration that you could add to a class except for
declarations that would change the representation of the type. This means
you can add methods, functions, interface implementations, and aliases, but
not fields, base classes, or virtual functions. The specific implementations
of virtual functions are part of the type representation, and so no virtual
functions may be overridden in an adapter either.
- The adapted type is compatible with the original type, and that relationship
is an equivalence class, so all of `Song`, `SongByTitle`, `FormattedSong`,
and `FormattedSongByTitle` end up compatible with each other.
- Since adapted types are compatible with the original type, you may
explicitly cast between them, but there is no implicit conversion between
these types.
Inside an adapter, the `Self` type matches the adapter. Members of the original
type may be accessed either by a cast:
```carbon
class SongByTitle {
adapt Song;
extend impl as Ordered {
fn Less(self, rhs: Self) -> bool {
return (self as Song).Title() < (rhs as Song).Title();
}
}
}
```
or using a qualified member access expression:
```carbon
class SongByTitle {
adapt Song;
extend impl as Ordered {
fn Less(self, rhs: Self) -> bool {
return self.(Song.Title)() < rhs.(Song.Title)();
}
}
}
```
**Comparison with other languages:** This matches the Rust idiom called
"newtype", which is used to implement traits on types while avoiding
[coherence](terminology.md#coherence) problems, see
[here](https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#using-the-newtype-pattern-to-implement-external-traits-on-external-types)
and
[here](https://github.com/Ixrec/rust-orphan-rules#user-content-why-are-the-orphan-rules-controversial).
Rust's mechanism doesn't directly support reusing implementations, though some
of that is provided by macros defined in libraries. Haskell has a
[`newtype` feature](https://wiki.haskell.org/Newtype) as well. Haskell's feature
doesn't directly support reusing implementations either, but the most popular
compiler provides it as
[an extension](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/newtype_deriving.html).
For the definition of adapters, including what declarations can be added to an
adapter, compatibility rules, member access, and casting between adapted types,
see [adapters in the class design](/docs/design/classes.md#adapters).
### Adapter compatibility
@@ -1952,7 +1901,7 @@ the API of the original type. The two most common cases expected are adding and
replacing an interface implementation. Users would indicate that an adapter
starts from the original type's existing API by using the `extend` keyword
before `adapt`, which
[extends member access to lookup names in the adapted class](../expressions/member_access.md#extend)
[extends member access to lookup names in the adapted class](/docs/design/expressions/member_access.md#extend)
along with `impl` lookup:
```carbon
@@ -2216,21 +2165,6 @@ fn Render(w: Window) {
}
```
### Future work: Adapter with stricter invariants
**Future work:** Rust also uses the newtype idiom to create types with
additional invariants or other information encoded in the type
([1](https://doc.rust-lang.org/rust-by-example/generics/new_types.html),
[2](https://doc.rust-lang.org/book/ch19-04-advanced-types.html#using-the-newtype-pattern-for-type-safety-and-abstraction),
[3](https://www.worthe-it.co.za/blog/2020-10-31-newtype-pattern-in-rust.html)).
This is used to record in the type system that some data has passed validation
checks, like `ValidDate` with the same data layout as `Date`. Or to record the
units associated with a value, such as `Seconds` versus `Milliseconds` or `Feet`
versus `Meters`. We should have some way of restricting the casts between a type
and an adapter to address this use case. One possibility would be to add the
keyword `private` before `adapt`, so you might write
`extend private adapt Date;`.
## Associated constants
> **TODO:** Update this section to reflect the new rules and guidance on
@@ -2741,7 +2675,7 @@ member of another. The `where` operator is not associative, so a type expression
using multiple must use round parens `(`...`)` to specify grouping.
The scope of a facet type formed by a `where` declaration
[extends](../expressions/member_access.md#extend) the scope of its first
[extends](/docs/design/expressions/member_access.md#extend) the scope of its first
operand, and the resulting facet type is complete if that scope it extends is
complete.
@@ -4007,7 +3941,7 @@ Given a type `U`, define the facet type `CompatibleWith(U)` as follows:
> `CompatibleWith(U)` is a facet type whose values are facets `T` such that
> `T as type` and `U as type` are
> [compatible types](terminology.md#compatible-types). That is values of `T` and
> [compatible types](/docs/design/classes.md#compatible-types). That is values of `T` and
> `U` as types can be cast back and forth without any change in representation
> (for example `T` is an [adapter](#adapting-types) for `U`).
@@ -5475,7 +5409,7 @@ An incomplete `C` cannot be used in the following contexts:
- ❌ `interface `...` { extend require impls C; }` or
`constraint `...` { extend require impls C; }`
- An `extend` declaration requires the target scope to be complete. See
[`extend` in member access](../expressions/member_access.md#extend).
[`extend` in member access](/docs/design/expressions/member_access.md#extend).
- ❌ `T: C` ... `T impls A` where `T` is a checked binding, and `A` is an
interface or named constraint different from `C`
- Need to see the definition of `C` to see if it implies `A`.
+2 -2
View File
@@ -526,8 +526,8 @@ cast from `T` to `CDCover`.
### Adapting types
Carbon has a mechanism called [adapting types](terminology.md#adapting-a-type)
to create new types that are [compatible](terminology.md#compatible-types) with
Carbon has a mechanism called [adapting types](/docs/design/classes.md#adapters)
to create new types that are [compatible](/docs/design/classes.md#compatible-types) with
existing types but with different interface implementations. This could be used
to add or replace implementations, or define implementations for reuse.
+5 -44
View File
@@ -38,10 +38,8 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
- [Member access](#member-access)
- [Simple member access](#simple-member-access)
- [Qualified member access expression](#qualified-member-access-expression)
- [Compatible types](#compatible-types)
- [Subtyping and casting](#subtyping-and-casting)
- [Coherence](#coherence)
- [Adapting a type](#adapting-a-type)
- [Type erasure](#type-erasure)
- [Archetype](#archetype)
- [Extending an interface](#extending-an-interface)
@@ -478,7 +476,7 @@ members of the interface as named members of the type. This means that the
members of the interface are available by way of both
[simple member access and qualified member access expressions](#member-access).
See
[how `extend` affects member access](../expressions/member_access.md#extend).
[how `extend` affects member access](/docs/design/expressions/member_access.md#extend).
If a type implements an interface without extending, the members of the
interface may only be accessed using
@@ -522,22 +520,6 @@ member access expression `s1.(Comparable.Less)(s2)`.
This form may be used to access any member of an interface implemented for a
type, whether or not it [extends the implementation](#extending-an-impl).
## Compatible types
Two types are compatible if they have the same notional set of values and
represent those values in the same way, even if they expose different APIs. The
representation of a type describes how the values of that type are represented
as a sequence of bits in memory. The set of values of a type includes properties
that the compiler can't directly see, such as invariants that the type
maintains.
We can't just say two types are compatible based on structural reasons. Instead,
we have specific constructs that create compatible types from existing types in
ways that encourage preserving the programmer's intended semantics and
invariants, such as implementing the API of the new type by calling (public)
methods of the original API, instead of accessing any private implementation
details.
## Subtyping and casting
Both subtyping and casting are different names for changing the type of a value
@@ -569,14 +551,14 @@ make it clear that the data representation of the value is not changing, just
its type as reflected in the API available to manipulate the value.
Casting is indicated explicitly by way of some syntax in the source code. You
might use a cast to switch between [type adaptations](#adapting-a-type), or to
might use a cast to switch between [type adaptations](/docs/design/classes.md#adapters), or to
be explicit where an implicit conversion would otherwise occur. For now, we are
saying "`x as y`" is the provisional syntax in Carbon for casting the value `x`
to the type `y`. Note that outside of generics, the term "casting" includes any
explicit type change, including those that change the data representation.
In contexts where an expression of one type is provided and a different type is
required, an [implicit conversion](../expressions/implicit_conversions.md) is
required, an [implicit conversion](/docs/design/expressions/implicit_conversions.md) is
performed if it is considered safe to do so. Such an implicit conversion, if
permitted, always has the same meaning as an explicit cast.
@@ -607,27 +589,6 @@ This is enforced using two kinds of rules:
The rationale for Carbon choosing coherence and alternatives considered may be
found in [this appendix](appendix-coherence.md)
## Adapting a type
A type can be adapted by creating a new type that is
[compatible](#compatible-types) with an existing type, but has a different API.
In particular, the new type might implement different interfaces or provide
different implementations of the same interfaces.
Unlike extending a type (as with C++ class inheritance), you are not allowed to
add new data fields onto the end of the representation -- you may only change
the API. This means that it is safe to [cast](#subtyping-and-casting) a value
between those two types without any dynamic checks or danger of
[object slicing](https://en.wikipedia.org/wiki/Object_slicing).
This is called "newtype" in Rust, and is used for capturing additional
information in types to improve type safety by moving some checking to compile
time ([1](https://doc.rust-lang.org/rust-by-example/generics/new_types.html),
[2](https://doc.rust-lang.org/book/ch19-04-advanced-types.html#using-the-newtype-pattern-for-type-safety-and-abstraction),
[3](https://www.worthe-it.co.za/blog/2020-10-31-newtype-pattern-in-rust.html))
and as a workaround for
[Rust's orphan rules for coherence](https://github.com/Ixrec/rust-orphan-rules#why-are-the-orphan-rules-controversial).
## Type erasure
"Type erasure" is where a type's API is replaced by a subset. Everything outside
@@ -664,7 +625,7 @@ An interface can be extended by defining an interface that includes the full API
of another interface, plus some additional API. Types implementing the extended
interface should automatically be considered to have implemented the narrower
interface. See
[how `extend` affects member access](../expressions/member_access.md#extend).
[how `extend` affects member access](/docs/design/expressions/member_access.md#extend).
## Dynamic-dispatch witness table
@@ -863,7 +824,7 @@ express, for example:
element type.
- An interface may define an associated facet that needs to be constrained to
implement some interfaces.
- This type must be [compatible](#compatible-types) with another type. You
- This type must be [compatible](/docs/design/classes.md#compatible-types) with another type. You
might use this to define alternate implementations of a single interfaces,
such as sorting order, for a single type.
+1 -1
View File
@@ -991,7 +991,7 @@ Each of the conversions described in this section is explicit if and only if it
invokes another explicit type conversion. Otherwise, it is implicit.
A type conversion of an expression with primitive extended type to a
[compatible type](generics/terminology.md#compatible-types) just re-interprets
[compatible type](classes.md#compatible-types) just re-interprets
the expression's result with a new type, so it requires no run-time work, and
has the same category as the input expression.
@@ -187,8 +187,8 @@ class C {
Each member of `C` with a distinct name will have a corresponding type (like
`__TypeOf_C_F`) and value of that type (like `__C_F`). There are two more types
for each member function (either static class function or method), though, that
[adapt](/docs/design/generics/terminology.md#adapting-a-type) `C` and represent
the type of binding that member with either a `C` value or variable.
[adapt](/docs/design/classes.md#adapters) `C` and represent the type of binding
that member with either a `C` value or variable.
```carbon
class __TypeOf_C_F {}