|
|
@@ -0,0 +1,168 @@
|
|
|
+# Generics details 7: final impls
|
|
|
+
|
|
|
+<!--
|
|
|
+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/983)
|
|
|
+
|
|
|
+<!-- toc -->
|
|
|
+
|
|
|
+## Table of contents
|
|
|
+
|
|
|
+- [Problem](#problem)
|
|
|
+- [Background](#background)
|
|
|
+- [Proposal](#proposal)
|
|
|
+- [Details](#details)
|
|
|
+- [Rationale based on Carbon's goals](#rationale-based-on-carbons-goals)
|
|
|
+- [Alternatives considered](#alternatives-considered)
|
|
|
+ - [No `final`, all parameterized impls may be specialized](#no-final-all-parameterized-impls-may-be-specialized)
|
|
|
+ - [`final` associated constants instead of `final` impls](#final-associated-constants-instead-of-final-impls)
|
|
|
+
|
|
|
+<!-- tocstop -->
|
|
|
+
|
|
|
+## Problem
|
|
|
+
|
|
|
+Allowing an impl to be specialized can lead to higher performance if there are
|
|
|
+parameter values for which a more optimized version can be written. However, not
|
|
|
+all impls will be specialized and there are some benefits when that is known:
|
|
|
+
|
|
|
+- The values of associated types can be assumed to come from the impl. In many
|
|
|
+ cases this means leaking fewer implementation details into the signature of
|
|
|
+ a function using generics.
|
|
|
+- The bodies of functions from the impl could be inlined into the caller even
|
|
|
+ when using a more dynamic implementation strategy rather than
|
|
|
+ monomorphization.
|
|
|
+
|
|
|
+However, not all impls can opt-out of specialization, since this can create
|
|
|
+incompatibilities between unrelated libraries. For example, consider two
|
|
|
+libraries that both import parameterized type `TA` and interface `I`:
|
|
|
+
|
|
|
+- Library `LB` that defines type `TB` can define an impl with type structure
|
|
|
+ `impl TA(TB, ?) as I`.
|
|
|
+- Library `LC` that defines type `TC` can define an impl with type structure
|
|
|
+ `impl TA(?, TC) as I`.
|
|
|
+
|
|
|
+Both of these are allowed under
|
|
|
+[Carbon's current orphan rules](/docs/design/generics/details.md#orphan-rule). A
|
|
|
+library `LD` that imports both `LB` and `LC` could then query for the
|
|
|
+implementation of `I` by `TA(TB, TC)` and would use the definition from library
|
|
|
+`LB`, which would be a conflict if library `LC` marked its impl definition as
|
|
|
+not specializable.
|
|
|
+
|
|
|
+## Background
|
|
|
+
|
|
|
+Rust currently does not support specialization, so for backwards compatibility
|
|
|
+impls are final by default in Rust's specialization proposal.
|
|
|
+
|
|
|
+## Proposal
|
|
|
+
|
|
|
+We propose that impls can be declared `final`, but only in libraries that must
|
|
|
+be imported by any file that would otherwise be able to define a higher-priority
|
|
|
+impl.
|
|
|
+
|
|
|
+## Details
|
|
|
+
|
|
|
+Details are in
|
|
|
+[the added `final` impl section to the generics details design document](/docs/design/generics/details.md#final-impls).
|
|
|
+
|
|
|
+## Rationale based on Carbon's goals
|
|
|
+
|
|
|
+This proposal supports the following of Carbon's goals:
|
|
|
+
|
|
|
+- [Performance-critical software](/docs/project/goals.md#performance-critical-software):
|
|
|
+ the ability to inline functions defined in `final` impls will in some cases
|
|
|
+ improve performance.
|
|
|
+- [Software and language evolution](/docs/project/goals.md#software-and-language-evolution):
|
|
|
+ reducing how much implementation details are exposed in a generic function's
|
|
|
+ signature allows that function to evolve.
|
|
|
+- [Code that is easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write):
|
|
|
+ reducing the list of requirements in a generic function signature is an
|
|
|
+ improvement to both readability and writability. Furthermore, `final` impls
|
|
|
+ are a tool for making code more predictable. For example, making the
|
|
|
+ dereferencing impl for pointers final means it always does the same thing
|
|
|
+ and produces a value of the expected type.
|
|
|
+
|
|
|
+## Alternatives considered
|
|
|
+
|
|
|
+### No `final`, all parameterized impls may be specialized
|
|
|
+
|
|
|
+In addition to the [problems listed above](#problem), we ran into problems in
|
|
|
+proposal [#911](https://github.com/carbon-language/carbon-lang/pull/911) trying
|
|
|
+to use the `CommonType` interface to define the type of a conditional
|
|
|
+expression, `if <condition> then <true-result> else <false-result>`. The idea is
|
|
|
+that `CommonType` implementations would specify how to combine the types of the
|
|
|
+`<true-result>` and `<false-result>` expressions using an associated type. In
|
|
|
+generic code, however, there was nothing to guarantee that there wouldn't be a
|
|
|
+specialization that would change the result. As a result, nothing could be
|
|
|
+concluded about the common type if either expression was generic. If at least
|
|
|
+the common type of two equal types was guaranteed, then you could use an
|
|
|
+explicit cast to make sure the types were as expected. Some method of limiting
|
|
|
+specialization was needed.
|
|
|
+
|
|
|
+We considered other approaches, such as using the fact that the compiler could
|
|
|
+see all implementations of private interfaces, but that didn't address other use
|
|
|
+cases. For example, we don't want users to be able to customize dereferencing
|
|
|
+pointers for their types so that dereferencing pointers behaves predictably in
|
|
|
+generic and regular code.
|
|
|
+
|
|
|
+### `final` associated constants instead of `final` impls
|
|
|
+
|
|
|
+We considered allowing developers to mark individual items in an impl as `final`
|
|
|
+instead. This gave developers more control, but we didn't have examples where
|
|
|
+that extra control was needed. It also introduced a number of complexities and
|
|
|
+concerns.
|
|
|
+
|
|
|
+The value for a `final let` could be an expression dependent on other associated
|
|
|
+constants which could be `final` or not. Checking that a refining impl adheres
|
|
|
+to that constraint is possible, but subtle and possibly tricky to diagnose
|
|
|
+mistakes clearly.
|
|
|
+
|
|
|
+If an impl matches a subset of an impl with a `final let`, how should the
|
|
|
+narrower impl comply with the restriction from the broader?
|
|
|
+
|
|
|
+```
|
|
|
+interface A {
|
|
|
+ let T:! type;
|
|
|
+}
|
|
|
+
|
|
|
+impl [U:! Type] Vector(U) as A {
|
|
|
+ final let T:! Type = i32;
|
|
|
+}
|
|
|
+
|
|
|
+impl Vector(f32) as A {
|
|
|
+ // T has to be `i32` because of the `final let`
|
|
|
+ // from the previous impl. What needs to be
|
|
|
+ // written here?
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+We considered two different approaches, neither of which was satisfying:
|
|
|
+
|
|
|
+- **Restate approach:** It could restate the `let` with a consistent value.
|
|
|
+ This does not give any indication that the `let` value is constrained, and
|
|
|
+ what impl is introducing that constraint, leading to spooky action at a
|
|
|
+ distance. It was unclear to us whether the restated `let` should use `final`
|
|
|
+ as well, or maybe some other keyword?
|
|
|
+- **Inheritance approach:** We could have a concept of inheriting from an
|
|
|
+ impl, and require that any impl refining an impl with `final` members must
|
|
|
+ inherit those values rather than declaring them. Inheritance between impls
|
|
|
+ might be a useful feature in its own right, but requires there be some way
|
|
|
+ to name the impl being inherited from.
|
|
|
+
|
|
|
+Consider two overlapping impls that both use `final let`. The compiler would
|
|
|
+need to validate that they are consistent on their overlap, a source of
|
|
|
+complexity for the user. An impl that overlaps both would have to be consistent
|
|
|
+with both, but would not be able to inherit from both, a problem with using the
|
|
|
+inheritance approach.
|
|
|
+
|
|
|
+Ultimately we decided that this approach had a lot of complexity, concerns, and
|
|
|
+edge cases and we could postpone trying to solve these problems until such time
|
|
|
+as we determined there was a need for the greater expressivity of being able to
|
|
|
+mark individual items as `final`. This discussion occurred in:
|
|
|
+
|
|
|
+- [Document examining an extended example using specialization](https://docs.google.com/document/d/1w-kRC338Jc1ibTu7Vf0pOlGKdrpumfz63bzUIxEj9jY/edit)
|
|
|
+- [2021-11-29 open discussion](https://docs.google.com/document/d/1cRrhRrmaUf2hVi2lFcHsYo2j0jI6t9RGZoYjWhRxp14/edit?resourcekey=0-xWHBEZ8zIqnJiB4yfBSLfA#heading=h.6komy889g3hc)
|
|
|
+- [Carbon's #typesystem channel on Discord](https://discord.com/channels/655572317891461132/708431657849585705/910681126236987495)
|