mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 20:00:12 +01:00
Update broken links to aid in following links while perusing the documentation. Closes #3778 I don't imagine I got every one of these right. Some 404s may be reported due to permissions. Using the lychee command in #3778, only the following link is reported as not found, but it's just a permission issue: ``` [proposals/p1367.md]: ✗ [404] https://github.com/carbon-language/carbon-lang/settings/access | Failed: Network error: Not Found ``` I tried to get it down to as few as possible, partially so that future link scans won't run into repeat errors. --------- Co-authored-by: Jon Ross-Perkins <jperkins@google.com> Co-authored-by: Carbon Infra Bot <carbon-external-infra@google.com>
688 lines
36 KiB
Markdown
688 lines
36 KiB
Markdown
# 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)
|
|
- [Compile-time parameters](#compile-time-parameters)
|
|
- [Interfaces](#interfaces)
|
|
- [Templates](#templates)
|
|
- [Checked-generic goals](#checked-generic-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)
|
|
- [Checked generics instead of open overloading and ADL](#checked-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)
|
|
- [Path from regular functions](#path-from-regular-functions)
|
|
- [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 with checked generics](#what-we-are-not-doing-with-checked-generics)
|
|
- [Not the full flexibility of templates](#not-the-full-flexibility-of-templates)
|
|
- [Checked generics will be checked when defined](#checked-generics-will-be-checked-when-defined)
|
|
- [Implementation strategy](#implementation-strategy)
|
|
- [References](#references)
|
|
|
|
<!-- 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
|
|
|
|
Carbon will support both
|
|
[checked and template generics](terminology.md#checked-versus-template-parameters)
|
|
to support generic programming by way of
|
|
[compile-time parameterization of language constructs](terminology.md#generic-means-compile-time-parameterized).
|
|
|
|
Carbon's checked generics will feature
|
|
[early type checking](terminology.md#early-versus-late-type-checking) and
|
|
[complete definition checking](terminology.md#complete-definition-checking).
|
|
|
|
Carbon's [template generics](#templates), in contrast, will more closely follow
|
|
the
|
|
[compile-time duck typing](https://en.wikipedia.org/wiki/Duck_typing#Templates_or_generic_types)
|
|
approach of C++ templates.
|
|
|
|
### Compile-time parameters
|
|
|
|
Generic functions and generic types will all take some compile-time parameters,
|
|
which will frequently be types, and in some cases will be
|
|
[deduced](terminology.md#deduced-parameter) from the types of the values of
|
|
explicit parameters.
|
|
|
|
If a compile-time 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 compile-time 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 compile-time 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 compile-time type parameter. In
|
|
Carbon we express these "type constraints" by saying we restrict to types that
|
|
implement specific [_interfaces_](terminology.md#interface). 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 compile-time type argument to a function that has that interface as a
|
|
requirement of its compile-time 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)
|
|
|
|
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.
|
|
|
|
## 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.
|
|
|
|
Carbon will have direct support for templates in addition to checked generics.
|
|
Carbon's template system will be similar to C++ templates with some specific
|
|
changes:
|
|
|
|
- It may have some limitations to be more compatible with checked generics,
|
|
much like how we
|
|
[restrict overloading](#checked-generics-instead-of-open-overloading-and-adl).
|
|
- 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.
|
|
|
|
Other aspects of Carbon will reduce the number of situations where errors will
|
|
only be detected after the template is
|
|
[instantiated](terminology.md#instantiation):
|
|
|
|
- Carbon's grammar won't require type information to disambiguate parsing, and
|
|
so definitions may be parsed without knowing the values of template
|
|
arguments.
|
|
- Carbon's name resolution is more restricted. Every package has its own
|
|
namespace, and there will be no
|
|
[argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl).
|
|
|
|
## Checked-generic goals
|
|
|
|
Our goal for checked 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 aiming for, here are some specific use
|
|
cases we expect Carbon checked 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. Checked
|
|
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
|
|
checked generics. More generally, Carbon checked generics will provide an
|
|
alternative for those situations inheritance doesn't handle as well. As a
|
|
specific example, we would like Carbon checked 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 checked generics. This extends outside the
|
|
realm of testing, allowing general configuration of how dependencies can be
|
|
satisfied. For example, checked 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).
|
|
|
|
#### Checked 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's
|
|
definition before instantiation.
|
|
|
|
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 checked-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. This is encapsulated in Carbon's
|
|
["one static open extension mechanism" principle](/docs/project/principles/static_open_extension.md).
|
|
As a consequence, Carbon checked 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. Checked 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 checked 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 checked 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 checked 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 checked 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 checked-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 checked-generic
|
|
function may be type checked using only its declaration, not its body.
|
|
|
|
### Predictability
|
|
|
|
A general property of checked 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 checked 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
|
|
checked generics systems
|
|
[have undecidable aspects to their type system](https://web.archive.org/web/20231223005655/https://3fx.ch/typing-is-hard.html),
|
|
including [Rust](https://sdleffler.github.io/RustTypeSystemTuringComplete/) and
|
|
Swift ([1](https://forums.swift.org/t/swift-type-checking-is-undecidable/39024),
|
|
[2](https://forums.swift.org/t/termination-checking-for-type-substitution/64504),
|
|
[3](https://forums.swift.org/t/two-more-undecidable-problems-in-the-swift-type-system/64814),
|
|
[4](https://forums.swift.org/t/brainf-in-the-swift-type-system/68301)). 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
|
|
checked-generic functions:
|
|
|
|
- Static strategy: Like template parameters, the values for checked parameters
|
|
must be statically known at the callsite, or known to be a compile-time
|
|
parameter to the calling function. This can generate separate, specialized
|
|
versions of each combination of checked 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 checked generic functions can be compiled using the static
|
|
specialization strategy. For example, the values for checked parameters must be
|
|
statically known at the callsite. Other limitations are
|
|
[listed below](#implementation-strategy).
|
|
|
|
**Nice to have:** It is desirable that the majority of functions with checked
|
|
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 checked 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 class layout. For example, packing a `bool` into the lower
|
|
bits of a pointer, or packing bit-fields with checked-parameter 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 checked parameters.
|
|
- Passing parameters to functions. We cannot pass values of types in
|
|
registers.
|
|
- Finding [specialized](terminology.md#specialization) implementations of
|
|
interfaces beyond those clearly needed from the function signature.
|
|
|
|
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
|
|
checked-generic code. The first step of migrating C++ template code would be to
|
|
first convert it to a [Carbon template](#templates). The problem is then how to
|
|
convert templates to checked generics within Carbon. This gives us these
|
|
sub-goals:
|
|
|
|
- Users should be able to convert a single template parameter to be checked at
|
|
a time. A hybrid function with both template and checked 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 checked 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 checked. 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
|
|
checked-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 checked 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 checked
|
|
generic without immediately updating all of the types used with the
|
|
template. For example, if the checked-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.
|
|
|
|
### Path from regular functions
|
|
|
|
Replacing a regular, non-parameterized function with a checked-generic function
|
|
should not affect existing callers of the function. There may be some
|
|
differences, such as when taking the address of the function, but ordinary calls
|
|
should not see any difference. In particular, the return type of a
|
|
checked-generic function should match, without any type erasure or additional
|
|
named members.
|
|
|
|
### Coherence
|
|
|
|
We want the generics system to have the
|
|
[_coherence_ property](terminology.md#coherence), so that the implementation of
|
|
an interface for a type is well defined. Since a checked-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 that are in tension with having an orphan rule limiting where
|
|
implementations may be defined. 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 _Ordered_ 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
|
|
checked-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)).
|
|
|
|
Alternatives to coherence are discussed in [an appendix](appendix-coherence.md).
|
|
|
|
### 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 checked-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 checked
|
|
generics. They both use nominal interfaces, were designed with checked 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](terminology.md#structural-interfaces), as
|
|
in Go, or [nominal](terminology.md#nominal-interfaces), 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 checked 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 `AddWith` interface
|
|
for any C++ type that implements `operator+`.
|
|
|
|
## What we are not doing with checked generics
|
|
|
|
What are we **not** doing with checked generics, particularly things that some
|
|
other languages do?
|
|
|
|
### Not the full flexibility of templates
|
|
|
|
Checked generics don't need to provide full flexibility of C++ templates:
|
|
|
|
- [Carbon templates](#templates) will cover those cases that don't fit inside
|
|
checked generics, such as code that relies on compile-time duck typing.
|
|
- We won't allow a specialization of some 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 checked generics. We expect to address those
|
|
use cases with metaprogramming or [templates](#templates) in Carbon.
|
|
- [Expression templates](https://en.wikipedia.org/wiki/Expression_templates)
|
|
are out of scope. It may be possible to express this approach in Carbon's
|
|
checked-generics system, but they won't drive any accommodation in the
|
|
checked-generics design.
|
|
|
|
### Checked 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 checked-generic
|
|
definitions.
|
|
|
|
### Implementation 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[T:! Ordered](list: List(T)) -> List(T) {
|
|
if (list.size() == 1) return list;
|
|
var chunks: List(List(T)) = FormChunks(list, sqrt(list.size()));
|
|
chunks = chunks.ApplyToEach(Sort);
|
|
chunks = Sort(chunks);
|
|
return MergeSortedListOfSortedLists(chunks);
|
|
}
|
|
```
|
|
|
|
This, given an implementation of `Ordered` for any list with elements that are
|
|
themselves `Ordered`, 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 checked-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.
|
|
|
|
## References
|
|
|
|
Proposals:
|
|
|
|
- [#24: Generics goals](https://github.com/carbon-language/carbon-lang/pull/24)
|
|
- [#950: Generic details 6: remove facets](https://github.com/carbon-language/carbon-lang/pull/950)
|
|
- [#2138: Checked and template generic terminology](https://github.com/carbon-language/carbon-lang/pull/2138)
|