mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 19:40:10 +01:00
Generics goals (#24)
The "generics" feature of Carbon is a large design effort that needs to be broken up into manageable steps. The first thing we need is a high-level goals document. The goals here reflect the desirable properties that we have discovered as part of considering several alternative generics designs:
- Use cases:
- Generic programming
- Upgrade path from C++ abstract interfaces
- Dependency injection
- Generics instead of open overloading and ADL
- Performance
- Better compiler experience
- Encapsulation
- Predictability
- Dispatch control
- Upgrade path from templates
- Coherence
- No novel name lookup
- Learn from others
- Interfaces are nominal
- Interop and evolution
- Bridge for C++ customization points
Goals are summarized in [this presentation](https://docs.google.com/presentation/d/12yGyu5Pvdag7CJp-_yLVkmbhyvuRIilBTNlkO0LKaHo/edit?usp=sharing&resourcekey=0-JB9yrUO4-6J8-zzGhnIyNg).
Co-authored-by: Chandler Carruth <chandlerc@gmail.com>
Co-authored-by: Jon Meow <46229924+jonmeow@users.noreply.github.com>
Co-authored-by: Matthew Riley <mdriley@gmail.com>
Co-authored-by: austern <austern@google.com>
Co-authored-by: Dmitri Gribenko <gribozavr@gmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Chandler Carruth
Jon Meow
Matthew Riley
austern
Dmitri Gribenko
parent
e022ff106f
commit
a6ddc03aa6
@@ -0,0 +1,17 @@
|
||||
# Carbon: Generics
|
||||
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
|
||||
This directory contains the collection of documents describing the generics
|
||||
feature of Carbon:
|
||||
|
||||
- ~~Overview~~ - not implemented yet
|
||||
- [Goals](goals.md) - The motivation and principles guiding the design
|
||||
direction.
|
||||
- ~~Terminology~~ - not implemented yet
|
||||
- ~~Detailed design~~ - not implemented yet
|
||||
- ~~Rejected alternatives~~ - not implemented yet
|
||||
@@ -0,0 +1,681 @@
|
||||
# Carbon: Generics goals
|
||||
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Purpose of this document](#purpose-of-this-document)
|
||||
- [Background](#background)
|
||||
- [Definition of generics](#definition-of-generics)
|
||||
- [Generic parameters](#generic-parameters)
|
||||
- [Interfaces](#interfaces)
|
||||
- [Relationship to templates](#relationship-to-templates)
|
||||
- [Goals](#goals)
|
||||
- [Use cases](#use-cases)
|
||||
- [Generic programming](#generic-programming)
|
||||
- [Upgrade path from C++ abstract interfaces](#upgrade-path-from-c-abstract-interfaces)
|
||||
- [Dependency injection](#dependency-injection)
|
||||
- [Generics instead of open overloading and ADL](#generics-instead-of-open-overloading-and-adl)
|
||||
- [Performance](#performance)
|
||||
- [Better compiler experience](#better-compiler-experience)
|
||||
- [Encapsulation](#encapsulation)
|
||||
- [Predictability](#predictability)
|
||||
- [Dispatch control](#dispatch-control)
|
||||
- [Upgrade path from templates](#upgrade-path-from-templates)
|
||||
- [Coherence](#coherence)
|
||||
- [No novel name lookup](#no-novel-name-lookup)
|
||||
- [Learn from others](#learn-from-others)
|
||||
- [Interfaces are nominal](#interfaces-are-nominal)
|
||||
- [Interop and evolution](#interop-and-evolution)
|
||||
- [Bridge for C++ customization points](#bridge-for-c-customization-points)
|
||||
- [What we are not doing](#what-we-are-not-doing)
|
||||
- [Not the full flexibility of templates](#not-the-full-flexibility-of-templates)
|
||||
- [Template use cases that are out of scope](#template-use-cases-that-are-out-of-scope)
|
||||
- [Generics will be checked when defined](#generics-will-be-checked-when-defined)
|
||||
- [Specialization strategy](#specialization-strategy)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
## Purpose of this document
|
||||
|
||||
This document attempts to clarify our goals for the design of the generics
|
||||
feature for Carbon. While these are not strict requirements, they represent the
|
||||
yardstick by which we evaluate design decisions. We do expect to achieve most of
|
||||
these goals, though some of these goals are somewhat aspirational or
|
||||
forward-looking.
|
||||
|
||||
## Background
|
||||
|
||||
### Definition of generics
|
||||
|
||||
**TODO** This should be replaced with a link to a terminology doc, once that is
|
||||
accepted.
|
||||
|
||||
C++ supports
|
||||
[parametric polymorphism](https://en.wikipedia.org/wiki/Parametric_polymorphism)
|
||||
and [generic programming](https://en.wikipedia.org/wiki/Generic_programming)
|
||||
through templates. Templates use
|
||||
[compile-time duck typing](https://en.wikipedia.org/wiki/Duck_typing#Templates_or_generic_types)
|
||||
to decide whether arguments are valid. This is a form of
|
||||
[structural typing](https://en.wikipedia.org/wiki/Structural_type_system) that
|
||||
is usage based.
|
||||
|
||||
Carbon will support _generics_ in order to allow definition checking in generic
|
||||
programming. Definition checking is accomplished by using
|
||||
[bounded parametric polymorphism](https://en.wikipedia.org/wiki/Parametric_polymorphism#Bounded_parametric_polymorphism)
|
||||
instead of compile-time duck typing. This means the legal arguments and the
|
||||
legal uses of a parameter are both goverened by explicit bounds on the parameter
|
||||
in a generic function's signature.
|
||||
|
||||
This would be _in addition_ to
|
||||
[template support in Carbon](#relationship-to-templates), if we decide to
|
||||
support templates in Carbon beyond interoperability with C++ templates.
|
||||
|
||||
### Generic parameters
|
||||
|
||||
Generic functions and generic types will all take some "generic parameters",
|
||||
which will frequently be types, and in some cases will be implicit or inferred
|
||||
from the types of the values of explicit parameters.
|
||||
|
||||
If a generic parameter is a type, the generic function's signature can specify
|
||||
constraints that the caller's type must satisfy. For example, a resizable array
|
||||
type (like C++'s `std::vector`) might have a generic type parameter with the
|
||||
constraint that the type must be movable and have a static size. A sort function
|
||||
might apply to any array whose elements are comparable and movable.
|
||||
|
||||
A constraint might involve multiple generic parameters. For example, a merge
|
||||
function might apply to two arbitrary containers so long as their elements have
|
||||
the same type.
|
||||
|
||||
### Interfaces
|
||||
|
||||
We need some way to express the constraints on a generic type parameter. In
|
||||
Carbon we express these "type constraints" by saying we restrict to types that
|
||||
implement specific _interfaces_. Interfaces describe an API a type could
|
||||
implement; for example, it might specify a set of functions, including names and
|
||||
signatures. A type implementing an interface may be passed as a generic type
|
||||
argument to a function that has that interface as a requirement of its generic
|
||||
type parameter. Then, the functions defined in the interface may be called in
|
||||
the body of the function. Further, interfaces have names that allow them to be
|
||||
reused.
|
||||
|
||||
Similar compile-time and run-time constructs may be found in other programming
|
||||
languages:
|
||||
|
||||
- [Rust's traits](https://doc.rust-lang.org/book/ch10-02-traits.html)
|
||||
- [Swift's protocols](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html)
|
||||
- [Java interfaces](<https://en.wikipedia.org/wiki/Interface_(Java)>)
|
||||
- [C++ concepts](<https://en.wikipedia.org/wiki/Concepts_(C%2B%2B)>)
|
||||
(compile-time only)
|
||||
- [Abstract base classes](<https://en.wikipedia.org/wiki/Class_(computer_programming)#Abstract_and_concrete>)
|
||||
in C++, etc. (run-time only)
|
||||
- [Go interfaces](https://gobyexample.com/interfaces) (run-time only)
|
||||
|
||||
In addition to specifying the methods available on a type, we may in the future
|
||||
expand the role of interfaces to allow other type constraints, such as on size,
|
||||
prefix of the data layout, specified method implementations, tests that must
|
||||
pass, etc. This might be part of making interfaces as expressive as classes, as
|
||||
part of a strategy to migrate to a future version of Carbon that uses interfaces
|
||||
instead of, rather than in addition to, standard inheritance-and-classes
|
||||
object-oriented language support. For the moment, everything beyond specifying
|
||||
the _methods_ available is out of scope.
|
||||
|
||||
### Relationship to templates
|
||||
|
||||
The entire idea of statically typed languages is that coding against specific
|
||||
types and interfaces is a better model and experience. Unfortunately, templates
|
||||
don't provide many of those benefits to programmers until it's too late, when
|
||||
users are consuming the API. Templates also come with high overhead, such as
|
||||
[template error messages](#better-compiler-experience).
|
||||
|
||||
We want Carbon code to move towards more rigorously type checked constructs.
|
||||
However, existing C++ code is full of unrestricted usage of compile-time
|
||||
duck-typed templates. They are incredibly convenient to write and so likely will
|
||||
continue to exist for a long time.
|
||||
|
||||
The question of whether Carbon has direct support for templates is out of scope
|
||||
for this document. The generics design is not completely separate from
|
||||
templates, so it is written as if Carbon will have its own templating system. It
|
||||
is assumed to be similar to C++ templates with some specific changes:
|
||||
|
||||
- It may have some limitations to be more compatible with generics, much like
|
||||
how we [restrict overloading](#generics-instead-of-open-overloading).
|
||||
- We likely will have a different method of selecting between different
|
||||
template instantiations, since
|
||||
[SFINAE](https://en.wikipedia.org/wiki/Substitution_failure_is_not_an_error)
|
||||
makes it difficult to deliver high quality compiler diagnostics.
|
||||
|
||||
We assume Carbon will have templates for a few different reasons:
|
||||
|
||||
- Carbon generics will definitely have to interact with _C++_ templates, and
|
||||
many of the issues will be similar.
|
||||
- We want to leave room in the design for templates, since it seems like it
|
||||
would be easier to remove templates if they are not pulling their weight
|
||||
than figure out how to add them in if they turn out to be needed.
|
||||
- We may want to have templates in Carbon as a temporary measure, to make it
|
||||
easier for users to transition off of C++ templates.
|
||||
|
||||
## Goals
|
||||
|
||||
Our goal for generics support in Carbon is to get most of the expressive
|
||||
benefits of C++ templates and open overloading with fewer downsides.
|
||||
Additionally, we want to support some dynamic dispatch use cases; for example,
|
||||
in cases that inheritance struggles with.
|
||||
|
||||
### Use cases
|
||||
|
||||
To clarify the expressive range we are aming for, here are some specific use
|
||||
cases we expect Carbon generics to cover.
|
||||
|
||||
#### Generic programming
|
||||
|
||||
We in particular want to support
|
||||
[generic programming](https://en.wikipedia.org/wiki/Generic_programming),
|
||||
including:
|
||||
|
||||
- Containers: arrays, maps, lists, and more complicated data structures like
|
||||
trees and graphs
|
||||
- Algorithms: sort, search
|
||||
- Wrappers: optional, variant, expected/result, smart pointers
|
||||
- Parameterized numeric types: `std::complex<T>`
|
||||
- Configurable and parametric APIs: the storage-customized `std::chrono` APIs
|
||||
- [Policy-based design](https://en.wikipedia.org/wiki/Modern_C%2B%2B_Design#Policy-based_design)
|
||||
|
||||
These would generally involve static, compile-time type arguments, and so would
|
||||
generally be used with [static dispatch](#dispatch-control).
|
||||
|
||||
#### Upgrade path from C++ abstract interfaces
|
||||
|
||||
Interfaces in C++ are often represented by abstract base classes. Generics
|
||||
should offer an alternative that does not rely on inheritance. This means looser
|
||||
coupling and none of the problems of multiple inheritance. Some people, such as
|
||||
[Sean Parent](https://sean-parent.stlab.cc/papers-and-presentations/#better-code-runtime-polymorphism),
|
||||
advocate for runtime polymorphism patterns in C++ that avoid inheritance because
|
||||
it can cause runtime performance, correctness, and code maintenance problems in
|
||||
some situations. Those patterns require a lot of boilerplate and complexity in
|
||||
C++. It would be nice if those patterns were simpler to express with Carbon
|
||||
generics. More generally, Carbon generics will provide an alternative for those
|
||||
situations inheritance doesn't handle as well. As a specific example, we would
|
||||
like Carbon generics to supplant the need to support multiple inheritance in
|
||||
Carbon.
|
||||
|
||||
This is a case that would use [dynamic dispatch](#dispatch-control).
|
||||
|
||||
#### Dependency injection
|
||||
|
||||
Types which only support subclassing for test stubs and mocks, as in
|
||||
["dependency injection"](https://en.wikipedia.org/wiki/Dependency_injection),
|
||||
should be able to easily migrate to generics. This extends outside the realm of
|
||||
testing, allowing general configuration of how dependencies can be satisfied.
|
||||
For example, generics might be used to configure how a library writes logs.
|
||||
|
||||
This would allow you to avoid the runtime overhead of virtual functions, using
|
||||
[static dispatch](#dispatch-control) without the
|
||||
[poor build experience of templates](#better-compiler-experience).
|
||||
|
||||
#### Generics instead of open overloading and ADL
|
||||
|
||||
One name lookup problem we would like to avoid is caused by open overloading.
|
||||
Overloading is where you provide multiple implementations of a function with the
|
||||
same name, and the implementation used in a specific context is determined by
|
||||
the argument types. Open overloading is overloading where the overload set is
|
||||
not restricted to a single file or library. This works with
|
||||
[Argument-dependent lookup](https://en.wikipedia.org/wiki/Argument-dependent_name_lookup),
|
||||
or [ADL](https://en.cppreference.com/w/cpp/language/adl), a mechanism for
|
||||
enabling open overloading without having to reopen the namespace where the
|
||||
function was originally defined. Together these enable
|
||||
[C++ customization points](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4381.html).
|
||||
|
||||
This is commonly used to provide a type-specific implementation of some
|
||||
operation, but doesn't provide any enforcement of consistency across the
|
||||
different overloads. It makes the meaning of code dependent on which overloads
|
||||
are imported, and is at odds with being able to type check a function
|
||||
generically.
|
||||
|
||||
Our goal is to address this use case, known more generally as
|
||||
[the expression problem](https://eli.thegreenplace.net/2016/the-expression-problem-and-its-solutions),
|
||||
with a generics mechanism that does enforce consistency so that type checking is
|
||||
possible without seeing all implementations. This will be Carbon's replacement
|
||||
for open overloading. As a consequence, Carbon generics will need to be able to
|
||||
support operator overloading.
|
||||
|
||||
A specific example is the absolute value function `Abs`. We would like to write
|
||||
`Abs(x)` for a variety of types. For some types `T`, such as `Int32` or
|
||||
`Float64`, the return type will be the same `T`. For other types, such as
|
||||
`Complex64` or `Quaternion`, the return type will be different. The generic
|
||||
functions that call `Abs` will need a way to specify whether they only operate
|
||||
on `T` such that `Abs` has signature `T -> T`.
|
||||
|
||||
This does create an issue when interoperating with C++ code using open
|
||||
overloading, which will
|
||||
[need to be addressed](#bridge-for-c-customization-points).
|
||||
|
||||
### Performance
|
||||
|
||||
For any real-world C++ template, there shall be an idiomatic reformulation in
|
||||
Carbon generics that has equal or better performance.
|
||||
[Performance is the top priority for Carbon](/docs/project/goals.md#performance-critical-software),
|
||||
and we expect to use generics pervasively, and so they can't compromise that
|
||||
goal in release builds.
|
||||
|
||||
**Nice to have:** There are cases where we should aim to do better than C++
|
||||
templates. For example, the additional structure of generics should make it
|
||||
easier to reduce generated code duplication, reducing code size and cache
|
||||
misses.
|
||||
|
||||
### Better compiler experience
|
||||
|
||||
Compared to C++ templates, we expect to reduce build times, particularly in
|
||||
development builds. We also expect the compiler to be able to report clearer
|
||||
errors, and report them earlier in the build process.
|
||||
|
||||
One source of improvement is that the bodies of generic functions and types can
|
||||
be type checked once when they are defined, instead of every time they are used.
|
||||
This is both a reduction in the total work done, and how errors can be reported
|
||||
earlier. On use, the errors can be a lot clearer since they will be of the form
|
||||
"argument did not satisfy function's contract as stated in its signature"
|
||||
instead of "substitution failed at this line of the function's implementation."
|
||||
|
||||
**Nice to have:** In development builds, we will have the option of using
|
||||
[dynamic dispatch](#dispatch-control) to reduce build times. We may also be able
|
||||
to reduce the amount of redundant compilation work even with the
|
||||
[static strategy](#dispatch-control) by identifying instantiations with the same
|
||||
arguments or identical implementations and only generating code for them once.
|
||||
|
||||
### Encapsulation
|
||||
|
||||
With a template, the implementation is part of the interface and types are only
|
||||
checked when the function is called and the template is instantiated.
|
||||
|
||||
A generic function is type checked when it is defined, and type checking can't
|
||||
use any information that is only known when the function is instantiated such as
|
||||
the exact argument types. Furthermore, calls to a generic function may be type
|
||||
checked using only its declaration, not its body. You should be able to call a
|
||||
generic function using only a forward declaration.
|
||||
|
||||
### Predictability
|
||||
|
||||
A general property of generics is they are more predictable than templates. They
|
||||
make clear when a type satisfies the requirements of a function; they have a
|
||||
documented contract. Further, that contract is enforced by the compiler, not
|
||||
sensitive to implementation details in the function body. This eases evolution
|
||||
by reducing (but not eliminating) the impact of
|
||||
[Hyrum's law](https://www.hyrumslaw.com/).
|
||||
|
||||
**Nice to have:** We also want well-defined boundaries between what is legal and
|
||||
not. This is "will my code be accepted by the compiler" predictability. We would
|
||||
prefer to avoid algorithms in the compiler with the form "run for up to N steps
|
||||
and report an error if it isn't resolved by then." For example, C++ compilers
|
||||
will typically have a template recursion limit. With generics, these problems
|
||||
arise due to trying to reason whether something is legal in all possible
|
||||
instantiations, rather than with specific, concrete types.
|
||||
|
||||
Some of this is likely unavoidable or too costly to avoid, as most existing
|
||||
generics systems
|
||||
[have undecidable aspects to their type system](https://3fx.ch/typing-is-hard.html),
|
||||
including [Rust](https://sdleffler.github.io/RustTypeSystemTuringComplete/) and
|
||||
[Swift](https://forums.swift.org/t/swift-type-checking-is-undecidable/39024). We
|
||||
fully expect there to be metaprogramming facilities in Carbon that will be able
|
||||
to execute arbitrary Turing machines, with infinite loops and undecidable
|
||||
stopping criteria. We don't see this as a problem though, just like we don't
|
||||
worry about trying to make the compiler reliably prevent you from writing
|
||||
programs that don't terminate.
|
||||
|
||||
We _would_ like to distinguish "the executed steps are present in the program's
|
||||
source" from "the compiler has to search for a proof that the code is legal." In
|
||||
the former case, the compiler can surface a problem to the user by pointing to
|
||||
lines of code in a trace of execution. The user could employ traditional
|
||||
debugging techniques to refine their understanding until they can determine a
|
||||
fix. What we want to avoid is the latter case, since it has bad properties:
|
||||
|
||||
- Error messages end up in the form: "this was too complicated to figure out,
|
||||
I eventually gave up."
|
||||
- Little in the way of actionable feedback on how to fix problems.
|
||||
- Not much the user can do to debug problems.
|
||||
- If the compiler is currently right at a limit for figuring something out, it
|
||||
is easy to imagine a change to a distant dependency can cause it to suddenly
|
||||
stop compiling.
|
||||
|
||||
If we can't find acceptable restrictions to make problems efficiently decidable,
|
||||
the next best solution is to require the proof to be in the source instead of
|
||||
derived by the compiler. If authoring the proof is too painful for the user, the
|
||||
we should invest in putting the proof search into IDEs or other tooling.
|
||||
|
||||
### Dispatch control
|
||||
|
||||
Enable simple user control of whether to use dynamic or static dispatch.
|
||||
|
||||
**Implementation strategy:** There are two strategies for generating code for
|
||||
generic functions:
|
||||
|
||||
- Static specialization strategy: Like template parameters, the values for
|
||||
generic parameters must be statically known at the callsite, or known to be
|
||||
a generic parameter to the calling function. This can generate separate,
|
||||
specialized versions of each combination of generic and template arguments,
|
||||
in order to optimize for those types or values.
|
||||
- Dynamic strategy: This is when the compiler generates a single version of
|
||||
the function that uses runtime dispatch to get something semantically
|
||||
equivalent to separate instantiation, but likely with different size, build
|
||||
time, and performance characteristics.
|
||||
|
||||
By default, we expect the implementation strategy to be controlled by the
|
||||
compiler, and not semantically visible to the user. For example, the compiler
|
||||
might use the static strategy for release builds and the dynamic strategy for
|
||||
development. Or it might choose between them on a more granular level based on
|
||||
code analysis, specific features used in the code, or profiling -- maybe some
|
||||
specific specializations are needed for performance, but others would just be
|
||||
code bloat.
|
||||
|
||||
We require that all generic functions can be compiled using the static
|
||||
specialization strategy. For example, the values for generic parameters must be
|
||||
statically known at the callsite. Other limitations are
|
||||
[listed below](#specialization-strategy).
|
||||
|
||||
**Nice to have:** It is desirable that the majority of functions with generic
|
||||
parameters also support the dynamic strategy. Specific features may prevent the
|
||||
compiler from using the dynamic strategy, but they should ideally be relatively
|
||||
rare, and easy to identify. Language features should avoid making it observable
|
||||
whether function code generated once or many times. For example, you should not
|
||||
be able to take the address of a function with generic parameters, or determine
|
||||
if a function was instantiated more than once using function-local static
|
||||
variables.
|
||||
|
||||
There are a few obstacles to supporting dynamic dispatch efficiently, which may
|
||||
limit the extent it is used automatically by implementations. For example, the
|
||||
following features would benefit substantially from guaranteed monomorphization:
|
||||
|
||||
- Field packing in struct layout. For example, packing a `Bool` into the lower
|
||||
bits of a pointer, or packing bit-fields with generic widths.
|
||||
- Allocating local variables in stack storage. Without monomorphization, we
|
||||
would need to perform dynamic memory allocation -- whether on the stack or
|
||||
the heap -- for local variables whose sizes depend on generic parameters.
|
||||
- Passing parameters to functions. We cannot pass values of generic types in
|
||||
registers.
|
||||
|
||||
While it is possible to address these with dynamic dispatch, handling some of
|
||||
them might have far-reaching and surprising performance implications. We don't
|
||||
want to compromise our goal for predictable performance.
|
||||
|
||||
We will allow the user to explicitly opt-in to using the dynamic strategy in
|
||||
specific cases. This could be just to control binary size in cases the user
|
||||
knows are not performance sensitive, or it could be to get the additional
|
||||
capability of operating on values with dynamic types. We may need to restrict
|
||||
this in various ways to maintain efficiency, like Rust does with object-safe
|
||||
traits.
|
||||
|
||||
We also anticipate that the user may want to force the compiler to use the
|
||||
static strategy in specific cases. This might be to keep runtime performance
|
||||
acceptable even when running a development or debug build.
|
||||
|
||||
### Upgrade path from templates
|
||||
|
||||
We want there to be a natural, incremental upgrade path from templated code to
|
||||
generic code.
|
||||
[Assuming Carbon will support templates directly](#relationship-to-templates),
|
||||
the first step of migrating C++ template code would be to first convert it to a
|
||||
Carbon template. The problem is then how to convert templates to generics within
|
||||
Carbon. This gives us these sub-goals:
|
||||
|
||||
- Users should be able to convert a single template parameter to be generic at
|
||||
a time. A hybrid function with both template and generic parameters has all
|
||||
the limitations of a template function: it can't be completely definition
|
||||
checked, it can't use the dynamic strategy, etc. Even so, there are still
|
||||
benefits from enforcing the function's declared contract for those
|
||||
parameters that have been converted.
|
||||
- Converting from a template parameter to a generic parameter should be safe.
|
||||
It should either work or fail to compile, never silently change semantics.
|
||||
- We should minimize the effort to convert functions and types from templated
|
||||
to generic. Ideally it should just require specifying the type constraints,
|
||||
affecting just the signature of the function, not its body.
|
||||
- **Nice to have:** It should be legal to call templated code from generic
|
||||
code when it would have the same semantics as if called from non-generic
|
||||
code, and an error otherwise. This is to allow more templated functions to
|
||||
be converted to generics, instead of requiring them to be converted
|
||||
specifically in bottom-up order.
|
||||
- **Nice to have:** Provide a way to migrate from a template to a generic
|
||||
without immediately updating all of the types used with the template. For
|
||||
example, if the generic code requires types to implement a new interface,
|
||||
one possible solution would use the original template code to provide an
|
||||
implementation for that interface for any type that structurally has the
|
||||
methods used by the original template.
|
||||
|
||||
If Carbon does not end up having direct support for templates, the transition
|
||||
will necessarily be less incremental.
|
||||
|
||||
### Coherence
|
||||
|
||||
We want the generics system to have the _coherence_ property. This means that
|
||||
there is a single answer to the question "what is the implementation of this
|
||||
interface for this type, if any?" independent of context, such as the libraries
|
||||
imported into a given file. Since a generic function only depends on interface
|
||||
implementations, they will always behave consistently on a given type,
|
||||
independent of context. For more on this, see
|
||||
[this description of what coherence is and why Rust enforces it](https://github.com/Ixrec/rust-orphan-rules#what-is-coherence).
|
||||
|
||||
Coherence greatly simplifies the language design, since it reduces the need for
|
||||
complicated rules to picking an implementation when there are many candidates.
|
||||
It also has a number of benefits for users:
|
||||
|
||||
- It removes a way packages can conflict with each other.
|
||||
- It makes the behavior of code more consistent and predictable.
|
||||
- It means there is no need to provide a disambiguation mechanism.
|
||||
Disambiguation is particularly problematic since the ambiguous call is often
|
||||
in generic code rather than code you control.
|
||||
- A consistent definition of a type is useful for instantiating a C++ or
|
||||
Carbon template on that type.
|
||||
|
||||
The main downside of coherence is that there are some capabilities we would like
|
||||
for interfaces which are in tension with the coherence property. For example, we
|
||||
would like to address
|
||||
[the expression problem](https://eli.thegreenplace.net/2016/the-expression-problem-and-its-solutions#another-clojure-solution-using-protocols).
|
||||
We can get some of the way there by allowing the implementation of an interface
|
||||
for a type to be defined with either the interface or the type. But some use
|
||||
cases remain:
|
||||
|
||||
- They should be some way of selecting between multiple implementations of an
|
||||
interface for a given type. For example, a _Song_ might support multiple
|
||||
orderings, such as by title or by artist. These would be represented by
|
||||
having multiple implementations of a _Comparable_ interface.
|
||||
- In order to allow libraries to be composed, there must be some way of saying
|
||||
a type implements an interface that is in another package that the authors
|
||||
of the type were unaware of. This is especially important since the library
|
||||
a type is defined in may not be able to see the interface definition without
|
||||
creating a dependency cycle or layering violation.
|
||||
|
||||
We should have some mechanism for addressing these use cases. There are multiple
|
||||
approaches that could work:
|
||||
|
||||
- Interface implementations could be external to types and are passed in to
|
||||
generic functions separately.
|
||||
- There could be some way to create multiple types that are compatible with a
|
||||
given value that you can switch between using casts to select different
|
||||
interface implementations. This is the approach used by Rust
|
||||
([1](https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#using-the-newtype-pattern-to-implement-external-traits-on-external-types),
|
||||
[2](https://github.com/Ixrec/rust-orphan-rules#user-content-why-are-the-orphan-rules-controversial)).
|
||||
- Carbon could support
|
||||
[scoped conformances](https://forums.swift.org/t/scoped-conformances/37159).
|
||||
|
||||
### No novel name lookup
|
||||
|
||||
We want to avoid adding rules for name lookup that are specific to generics.
|
||||
This is in contrast to Rust which has different lookup rules inside its traits.
|
||||
Instead, we should structure generics in a way that reuses existing name lookup
|
||||
facilities of the language.
|
||||
|
||||
**Nice to have:** One application of this that would be nice to have is if the
|
||||
names of a type's members were all determined by a type's definition. So if `x`
|
||||
has type `T`, then if you write `x.y` you should be able to look up `y` in the
|
||||
definition of `T`. This might need to be somewhat indirect in some cases. For
|
||||
example, if `T` inherits from `U`, the name `y` might come from `U` and not be
|
||||
mentioned in the definition of `T` directly. We may have similar mechanisms
|
||||
where `T` gets methods that have default implementations in interfaces it
|
||||
implements, as long as the names of those interfaces are explicitly mentioned in
|
||||
the definition of `T`.
|
||||
|
||||
### Learn from others
|
||||
|
||||
Many languages have implemented generics systems, and we should learn from those
|
||||
experiences. We should copy what works and makes sense in the context of Carbon,
|
||||
and change decisions that led to undesirable compromises. We are taking the
|
||||
strongest guidance from Rust and Swift, which have similar goals and significant
|
||||
experience with the implementation and usability of generics. They both use
|
||||
nominal interfaces, were designed with generics from the start, and produce
|
||||
native code. Contrast with Go which uses structural interfaces, or Java which
|
||||
targets a virtual machine that predated its generics feature.
|
||||
|
||||
For example, Rust has found that supporting defaults for interface methods is a
|
||||
valuable feature. It is useful for [evolution](#interop-and-evolution),
|
||||
implementation reuse, and for bridging the gap between the minimal functionality
|
||||
a type wants to implement and the rich API that users want to consume
|
||||
([example](https://doc.rust-lang.org/std/iter/trait.Iterator.html)).
|
||||
|
||||
We still have the flexibility to make simplifications that Rust cannot because
|
||||
they need to maintain compatibility. We could remove the concept of
|
||||
`fundamental` and explicit control over which methods may be specialized. These
|
||||
are complicated and
|
||||
[impose coherence restrictions](http://aturon.github.io/tech/2017/02/06/specialization-and-coherence/).
|
||||
|
||||
### Interfaces are nominal
|
||||
|
||||
Interfaces can either be structural, as in Go, or nominal, as in Rust and Swift.
|
||||
Structural interfaces match any type that has the required methods, whereas
|
||||
nominal interfaces only match if there is an explicit declaration stating that
|
||||
the interface is implemented for that specific type. Carbon will support nominal
|
||||
interfaces, allowing them to designate _semantics_ beyond the basic structure of
|
||||
the methods.
|
||||
|
||||
This means that interfaces implicitly specify the intended semantics and
|
||||
invariants of and between those functions. Unlike the function signatures, this
|
||||
contract is between the implementers and the consumers of interfaces and is not
|
||||
enforced by Carbon itself. For example, a `Draw` method would mean different
|
||||
things when it is part of a `GameResult` interface versus an `Image2D`
|
||||
interface, even if those methods happen to have the same signature.
|
||||
|
||||
### Interop and evolution
|
||||
|
||||
[Evolution is a high priority for Carbon](/docs/project/goals.md#software-and-language-evolution),
|
||||
and so will need mechanisms to support evolution when using generics. New
|
||||
additions to an interface might:
|
||||
|
||||
- need default implementations
|
||||
- be marked "upcoming" to allow for a period of transition
|
||||
- replace other APIs that need to be marked "deprecated"
|
||||
|
||||
Experience with C++ concepts has shown that interfaces are
|
||||
[hard to evolve](https://www.youtube.com/watch?v=v_yzLe-wnfk) without these
|
||||
kinds of supporting language mechanisms. Otherwise changes to interfaces need to
|
||||
made simultaneously with updates to types that implement the interface or
|
||||
functions that consume it.
|
||||
|
||||
Another way of supporting evolution is to allow one interface to be
|
||||
substitutable for another. For example, a feature that lets you use an
|
||||
implementation of `Interface1` for a type to automatically get an implementation
|
||||
of `Interface2`, as well as the other way around, would help transitioning
|
||||
between those two interfaces.
|
||||
|
||||
Evolution in particular means that the set of names in an interface can change,
|
||||
and so two interfaces that don't start with name conflicts can develop them.
|
||||
|
||||
To handle name conflicts, interfaces should be separate, isolated namespaces. We
|
||||
should provide mechanisms to allow one type to implement two interfaces that
|
||||
accidentally use the same name for different things, and for functions to use
|
||||
interfaces with name conflicts together on a single type. Contrast this with
|
||||
Swift, where a type can only supply one associated type of a given name even
|
||||
when implementing multiple protocols. Similarly a function in Swift with a given
|
||||
name and signature can only have a single implementation for a type.
|
||||
|
||||
Note this is possible since [interfaces are nominal](#interfaces-are-nominal).
|
||||
The place where types specify that they implement an interface is also the
|
||||
vehicle for unambiguously designating which function implementation goes with
|
||||
what interface.
|
||||
|
||||
### Bridge for C++ customization points
|
||||
|
||||
There will need to be some bridge for C++ extension points that currently rely
|
||||
on open overloading or
|
||||
[ADL](https://en.wikipedia.org/wiki/Argument-dependent_name_lookup). For
|
||||
example, we need some way for C++
|
||||
[customization points](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4381.html)
|
||||
like `swap` to work on Carbon types. We might define `CPlusPlus.ADL.swap` as a
|
||||
Carbon interface to be that bridge. Carbon types could implement that interface
|
||||
to work from C++, and Carbon functions could use that interface to invoke `swap`
|
||||
on C++ types.
|
||||
|
||||
Similarly, we will want some way to implement Carbon interfaces for C++ types.
|
||||
For example, we might have a template implementation of an `Addable` interface
|
||||
for any C++ type that implements `operator+`.
|
||||
|
||||
## What we are not doing
|
||||
|
||||
What are we **not** doing with generics, particularly things that some other
|
||||
languages do?
|
||||
|
||||
### Not the full flexibility of templates
|
||||
|
||||
Generics don't need to provide full flexibility of C++ templates:
|
||||
|
||||
- The current assumption is that
|
||||
[Carbon templates](#relationship-to-templates) will cover those cases that
|
||||
don't fit inside generics, such as code that relies on compile-time duck
|
||||
typing.
|
||||
- We won't allow a specialization of some generic interface for some
|
||||
particular type to actually expose a _different_ interface, with different
|
||||
methods or different types in method signatures. This would break modular
|
||||
type checking.
|
||||
- [Template metaprogramming](https://en.wikipedia.org/wiki/Template_metaprogramming)
|
||||
will not be supported by Carbon generics. We expect to address those use
|
||||
cases with metaprogramming or templates in Carbon.
|
||||
|
||||
### Template use cases that are out of scope
|
||||
|
||||
We will also not require Carbon generics to support
|
||||
[expression templates](https://en.wikipedia.org/wiki/Expression_templates),
|
||||
[variadics](https://en.wikipedia.org/wiki/Variadic_function), or
|
||||
[variadic templates](https://en.wikipedia.org/wiki/Variadic_template). Those are
|
||||
all out of scope. It would be fine for our generics system to support these
|
||||
features, but they won't drive any accommodation in the generics design, at
|
||||
least until we have some resolution about templates in Carbon.
|
||||
|
||||
### Generics will be checked when defined
|
||||
|
||||
C++ compilers must defer full type checking of templates until they are
|
||||
instantiated by the user. Carbon will not defer type checking of generic
|
||||
definitions.
|
||||
|
||||
### Specialization strategy
|
||||
|
||||
We want all generic Carbon code to support [static dispatch](#dispatch-control).
|
||||
This means we won't support unbounded type families. Unbounded type families are
|
||||
when recursion creates an infinite collection of types, such as in
|
||||
[this example from Swift](https://forums.swift.org/t/ergonomics-generic-types-conforming-in-more-than-one-way/34589/71)
|
||||
or:
|
||||
|
||||
```carbon
|
||||
fn Sort[Comparable T](List(T) list) -> List(T) {
|
||||
if (list.size() == 1) return list;
|
||||
var List(List(T)) chunks = FormChunks(list, sqrt(list.size()));
|
||||
chunks = chunks.ApplyToEach(Sort);
|
||||
chunks = Sort(chunks);
|
||||
return MergeSortedListOfSortedLists(chunks);
|
||||
}
|
||||
```
|
||||
|
||||
This, given an implementation of `Comparable` for any list with elements that
|
||||
are themselves `Comparable`, would recursively call itself to produce a set of
|
||||
types without bound. That is, calling `Sort` on a `List(Int)` would internally
|
||||
call `Sort` on a `List(List(Int))` and so on recursively without any static
|
||||
limit.
|
||||
|
||||
We won't require all generic Carbon code to support dynamic dispatch, but we
|
||||
would like it to be an implementation option for the compiler in the majority of
|
||||
cases.
|
||||
|
||||
Lastly, runtime specialization is out of scope as an implementation strategy.
|
||||
That is, some language runtimes JIT a specialization when it is first needed,
|
||||
but it is not a goal for Carbon to support such an implementation strategy.
|
||||
@@ -22,6 +22,7 @@ request:
|
||||
<!-- proposals -->
|
||||
<!-- Generated by ./scripts/update_proposal_list.py -->
|
||||
|
||||
- [0024 - Generics goals](p0024.md)
|
||||
- [0029 - Linear, rebase, and pull-request GitHub workflow](p0029.md)
|
||||
- [0042 - Create code review guidelines](p0042.md)
|
||||
- [0044 - Proposal tracking](p0044.md)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Generics goals
|
||||
|
||||
<!--
|
||||
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/24)
|
||||
|
||||
## Problem
|
||||
|
||||
The "generics" feature of Carbon is a large design effort that needs to be
|
||||
broken up into manageable steps. The first thing we need is a high-level goals
|
||||
document:
|
||||
|
||||
- to make sure we agree on the approach we are taking,
|
||||
- to clearly communicate expectations to contributors for future steps,
|
||||
- to encourage us to solve problems in the design in a consistent way,
|
||||
- to provide a yardstick for measuring different alternatives under
|
||||
consideration.
|
||||
|
||||
## Background
|
||||
|
||||
The question of what a generics feature would look like in Carbon has been an
|
||||
ongoing discussion, with many alternative proposals. Of course there have been a
|
||||
number of use cases that we want to address with this feature, but over the
|
||||
course of this process we have discovered a number of desirable properties we
|
||||
would like to achieve with any solution.
|
||||
|
||||
## Proposal
|
||||
|
||||
See the [generics goals document](/docs/design/generics/goals.md).
|
||||
|
||||
## Rationale
|
||||
|
||||
The goals here well reflect Carbon's goals as applied to the specifics of the
|
||||
generics feature.
|
||||
@@ -17,6 +17,13 @@ class TestProposal(unittest.TestCase):
|
||||
p = proposals.get_list(proposals_path)
|
||||
self.assertEqual(
|
||||
p[0],
|
||||
(
|
||||
"0024 - Generics goals",
|
||||
"p0024.md",
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
p[1],
|
||||
(
|
||||
"0029 - Linear, rebase, and pull-request GitHub workflow",
|
||||
"p0029.md",
|
||||
|
||||
Reference in New Issue
Block a user