This document is a high-level description of Carbon's generics design, with pointers to other design documents that dive deeper into individual topics.
The goal of Carbon generics is to provide an alternative to Carbon (or C++) templates. Generics in this form should provide many advantages, including:
For more detail, see the detailed discussion of generics goals and generics terminology.
Summary of how Carbon generics work:
& operation on type-of-types allows you conveniently combine
interfaces. It gives you all the names that don't conflict.Generics are a mechanism for writing parameterized code that applies generally instead of making near-duplicates for very similar situations, much like C++ templates. For example, instead of having one function per type-you-can-sort:
fn SortInt32Vector(a: Vector(Int32)*) { ... }
fn SortStringVector(a: Vector(String)*) { ... }
...
You might have one generic function that could sort any array with comparable elements:
fn SortVector(T:! Comparable, a: Vector(T)*) { ... }
The syntax above adds a ! to indicate that the parameter named T is generic.
Given an Int32 vector iv, SortVector(Int32, &iv) is equivalent to
SortInt32Vector(&iv). Similarly for a String vector sv,
SortVector(String, &sv) is equivalent to SortStringVector(&sv). Thus, we can
sort any vector containing comparable elements using this single SortVector
function.
This ability to generalize makes SortVector a generic.
The SortVector function requires a definition of Comparable, with the goal
that the compiler can:
In this example, then, Comparable is an interface.
Interfaces describe all the requirements needed for the type T. Given that the
compiler knows T satisfies those requirements, it can type check the body of
the SortVector function. This includes checking that the Comparable
requirement covers all of the uses of T inside the function.
Later, when the compiler comes across a call to SortVector, it can type check
against the requirements expressed in the function's signature. Using only the
types at the call site, the compiler can check that the member elements of the
passed-in array satisfy the function's requirements. There is no need to look at
the body of the SortVector function, since we separately checked that those
requirements were sufficient.
Interfaces, then, have a name and describe methods, functions, and other items for types to implement.
Example:
interface Comparable {
// `Less` is an associated method.
fn Less[me: Self](that: Self) -> Bool;
}
Interfaces describe functionality, but not data; no variables may be declared in an interface.
Contrast these generics with a C++ template, where the compiler may be able to do some checking given a function definition, but more checking of the definition is required after seeing the call sites once all the instantiations are known.
Note: The doc on Generics terminology goes into more detail about the differences between generics and templates.
Interfaces themselves only describe functionality by way of method descriptions. A type needs to implement an interface to indicate that it supports its functionality. A given type may implement an interface at most once.
Consider this interface:
interface Printable {
fn Print[me: Self]();
}
The interface keyword is used to define a
nominal interface. That means that types
need to explicitly implement them, using an impl block, such as here:
class Song {
// ...
// Implementing `Printable` for `Song` inside the definition of `Song`
// means all names of `Printable`, such as `F`, are included as a part
// of the `Song` API.
impl as Printable {
// Could use `Self` in place of `Song` here.
fn Print[me: Song]() { ... }
}
}
// Implement `Comparable` for `Song` without changing the API of `Song`
// using an `external impl` declaration. This may be defined in either
// the library defining `Song` or `Comparable`.
external impl Song as Comparable {
// Could use either `Self` or `Song` here.
fn Less[me: Self](that: Self) -> Bool { ... }
}
Implementations may be defined within the class definition itself or externally. External implementations may be defined in the library defining the interface.
The methods of an interface implemented within the class definition may be called with the unqualified syntax. All methods of implemented interfaces may be called with the qualified syntax, whether they are defined internally or externally.
var song: Song;
// `song.Print()` is allowed, unlike `song.Play()`.
song.Print();
// `Less` is defined in `Comparable`, which is implemented
// externally for `Song`
song.(Comparable.Less)(song);
// Can also call `Print` using the qualified syntax:
song.(Printable.Print)();
To type check a function, the compiler needs to be able to verify that uses of a
value match the capabilities of the value's type. In SortVector, the parameter
T is a type, but that type is a generic parameter. That means that the
specific type value assigned to T is not known when type checking the
SortVector function. Instead it is the constraints on T that let the
compiler know what operations may be performed on values of type T. Those
constraints are represented by the type of T, a
type-of-type.
In general, a type-of-type describes the capabilities of a type, while a type defines specific implementations of those capabilities.
An interface, like Comparable, may be used as a type-of-type. In that case,
the constraint on the type is that it must implement the interface Comparable.
A type-of-type also defines a set of names and a mapping to corresponding
qualified names. You may combine interfaces into new type-of-types using
the & operator or
structural interfaces.
We want to be able to call generic functions just like ordinary functions, and write generic function bodies like ordinary functions. There are only a few differences, like that you can't take the address of generic functions.
This SortVector function is explicitly providing type information that is
already included in the type of the second argument. To eliminate the argument
at the call site, use a deduced parameter.
fn SortVectorDeduced[T:! Comparable](a: Vector(T)*) { ... }
The T parameter is defined in square brackets before the explicit parameter
list in parenthesis to indicate it should be deduced. This means you may call
the function without the type argument, just like the ordinary functions
SortInt32Vector or SortStringVector:
SortVectorDeduced(&anIntVector);
// or
SortVectorDeduced(&aStringVector);
and the compiler deduces that the T argument should be set to Int32 or
String from the type of the argument.
Deduced arguments are always determined from the call and its explicit arguments. There is no syntax for specifying deduced arguments directly at the call site.
// ERROR: can't determine `U` from explicit parameters
fn Illegal[T:! Type, U:! Type](x: T) -> U { ... }
A function with a generic type parameter can have the same function body as an unparameterized one.
fn PrintIt[T:! Printable](p: T*) {
p->Print();
}
fn PrintIt(p: Song*) {
p->Print();
}
Inside the function body, you can treat the generic type parameter just like any other type. There is no need to refer to or access generic parameters differently because they are defined as generic, as long as you only refer to the names defined by type-of-type for the type parameter.
You may also refer to any of the methods of interfaces required by the type-of-type using the qualified syntax, as shown in the following sections.
A function can have a mix of generic, template, and regular parameters. Likewise, it's allowed to pass a template or generic value to a generic or regular parameter. However, passing a generic value to a template parameter is future work.
Interfaces can require other interfaces be implemented:
interface Equatable {
fn IsEqual[me: Self](that: Self) -> Bool;
}
// `Iterable` requires that `Equatable` is implemented.
interface Iterable {
impl as Equatable;
fn Advance[addr me: Self*]();
}
The extends keyword is used to extend
another interface. If interface Child extends interface Parent, Parent's
interface is both required and all its methods are included in Child's
interface.
// `Hashable` extends `Equatable`.
interface Hashable {
extends Equatable;
fn Hash[me: Self]() -> UInt64;
}
// `Hashable` is equivalent to:
interface Hashable {
impl as Equatable;
alias IsEqual = Equatable.IsEqual;
fn Hash[me: Self]() -> UInt64;
}
A type may implement the parent interface implicitly by implementing all the methods in the child implementation.
class Key {
// ...
impl as Hashable {
fn IsEqual[me: Key](that: Key) -> Bool { ... }
fn Hash[me: Key]() -> UInt64 { ... }
}
// No need to separately implement `Equatable`.
}
var k: Key = ...;
k.Hash();
k.IsEqual(k);
The & operation on type-of-types allows you conveniently combine interfaces.
It gives you all the names that don't conflict.
interface Renderable {
fn GetCenter[me: Self]() -> (Int, Int);
// Draw the object to the screen
fn Draw[me: Self]();
}
interface EndOfGame {
fn SetWinner[addr me: Self*](player: Int);
// Indicate the game was a draw
fn Draw[addr me: Self*]();
}
fn F[T:! Renderable & EndOfGame](game_state: T*) -> (Int, Int) {
game_state->SetWinner(1);
return game_state->Center();
}
Names with conflicts can be accessed using the qualified syntax.
fn BothDraws[T:! Renderable & EndOfGame](game_state: T*) {
game_state->(Renderable.Draw)();
game_state->(GameState.Draw)();
}
You may also declare a new type-of-type directly using "structural interfaces". Structural interfaces can express requirements that multiple interfaces be implemented, and give you control over how name conflicts are handled. Structural interfaces have other applications and capabilities not covered here.
structural interface Combined {
impl as Renderable;
impl as EndOfGame;
alias Draw_Renderable = Renderable.Draw;
alias Draw_EndOfGame = EndOfGame.Draw;
alias SetWinner = EndOfGame.SetWinner;
}
fn CallItAll[T:! Combined](game_state: T*, int winner) {
if (winner > 0) {
game_state->SetWinner(winner);
} else {
game_state->Draw_EndOfGame();
}
game_state->Draw_Renderable();
// Can still use qualified syntax for names
// not defined in the structural interface
return game_state->(Renderable.Center)();
}
Inside a generic function, the API of a type argument is erased except for the names defined in the type-of-type.
For example: If there were a class CDCover defined this way:
class CDCover {
impl as Printable {
...
}
}
it can be passed to this PrintIt function:
fn PrintIt[T:! Printable](p: T*) {
p->Print();
}
At that point, two erasures occur:
CDCover's API except Printable is erased during the cast from
CDCover to Printable, which is the facet type
CDCover as Printable.PrintIt, the type connection to CDCover is lost. Outside
of PrintIt you can cast a CDCover as Printable value back to CDCover.
Inside of PrintIt, you can't cast p or T back to CDCover.UInt size of an array
or tuple.upcoming or deprecated to
support evolution.