diff --git a/docs/design/generics/details.md b/docs/design/generics/details.md index bcce9dee81122..0c5f6dc6f4fa5 100644 --- a/docs/design/generics/details.md +++ b/docs/design/generics/details.md @@ -92,12 +92,14 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - [`final` impls](#final-impls) - [Libraries that can contain `final` impls](#libraries-that-can-contain-final-impls) - [Comparison to Rust](#comparison-to-rust) +- [Interface members with definitions](#interface-members-with-definitions) + - [Interface defaults](#interface-defaults) + - [`final` members](#final-members) - [Future work](#future-work) - [Dynamic types](#dynamic-types) - [Runtime type parameters](#runtime-type-parameters) - [Runtime type fields](#runtime-type-fields) - [Abstract return types](#abstract-return-types) - - [Interface defaults](#interface-defaults) - [Evolution](#evolution) - [Testing](#testing) - [Operator overloading](#operator-overloading) @@ -4270,6 +4272,162 @@ differences between the Carbon and Rust plans: ordering on type structures, picking one as higher priority even without one being more specific in the sense of only applying to a subset of types. +## Interface members with definitions + +Interfaces may provide definitions for members, such as a function body for an +associated function or method or a value for an associated constant. If these +definitions may be overridden in implementations, they are called "defaults." +Otherwise they are called "final members." + +### Interface defaults + +An interface may provide a default implementation of methods in terms of other +methods in the interface. + +``` +interface Vector { + fn Add[me: Self](b: Self) -> Self; + fn Scale[me: Self](v: f64) -> Self; + // Default definition of `Invert` calls `Scale`. + fn Invert[me: Self]() -> Self { + return me.Scale(-1.0); + } +} +``` + +An impl of that interface for a type may omit a definition of `Invert` to use +the default, or provide a definition to override the default. + +Interface defaults are helpful for [evolution](#evolution), as well as reducing +boilerplate. Defaults address the gap between the minimum necessary for a type +to provide the desired functionality of an interface and the breadth of API that +developers desire. As an example, in Rust the +[iterator trait](https://doc.rust-lang.org/std/iter/trait.Iterator.html) only +has one required method but dozens of "provided methods" with defaults. + +Defaults may also be provided for associated constants, such as associated +types, and interface parameters, using the `= ` syntax. + +``` +interface Add(Right:! Type = Self) { + let Result:! Type = Self; + fn DoAdd[me: Self](right: Right) -> Result; +} + +impl String as Add() { + // Right == Result == Self == String + fn DoAdd[me: Self](right: Self) -> Self; +} +``` + +Note that `Self` is a legal default value for an associated type or type +parameter. In this case the value of those names is not determined until `Self` +is, so `Add()` is equivalent to the constraint: + +``` +// Equivalent to Add() +constraint AddDefault { + extends Add(Self); +} +``` + +Note also that the parenthesis are required after `Add`, even when all +parameters are left as their default values. + +More generally, default expressions may reference other associated types or +`Self` as parameters to type constructors. For example: + +``` +interface Iterator { + let Element:! Type; + let Pointer:! Type = Element*; +} +``` + +Carbon does **not** support providing a default implementation of a required +interface. + +``` +interface TotalOrder { + fn TotalLess[me: Self](right: Self) -> Bool; + // ❌ Illegal: May not provide definition + // for required interface. + impl PartialOrder { + fn PartialLess[me: Self](right: Self) -> Bool { + return me.TotalLess(right); + } + } +} +``` + +The workaround for this restriction is to use a [blanket impl](#blanket-impls) +instead: + +``` +interface TotalOrder { + fn TotalLess[me: Self](right: Self) -> Bool; + impl PartialOrder; +} + +external impl [T:! TotalOrder] T as PartialOrder { + fn PartialLess[me: Self](right: Self) -> Bool { + return me.TotalLess(right); + } +} +``` + +Note that by the [orphan rule](#orphan-rule), this blanket impl must be defined +in the same library as `PartialOrder`. + +**Comparison with other languages:** Rust supports specifying defaults for +[methods](https://doc.rust-lang.org/book/ch10-02-traits.html#default-implementations), +[interface parameters](https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#default-generic-type-parameters-and-operator-overloading), +and +[associated constants](https://doc.rust-lang.org/reference/items/associated-items.html#associated-constants-examples). +Rust has found them valuable. + +### `final` members + +As an alternative to providing a definition of an interface member as a default, +members marked with the `final` keyword will not allow that definition to be +overridden in impls. + +``` +interface TotalOrder { + fn TotalLess[me: Self](right: Self) -> Bool; + final fn TotalGreater[me: Self](right: Self) -> Bool { + return right.TotalLess(me); + } +} + +class String { + impl as TotalOrder { + fn TotalLess[me: Self](right: Self) -> Bool { ... } + // ❌ Illegal: May not provide definition of final + // method `TotalGreater`. + fn TotalGreater[me: Self](right: Self) -> Bool { ... } + } +} + +interface Add(T:! Type = Self) { + // `AddWith` *always* equals `T` + final let AddWith:! Type = T; + // Has a *default* of `Self` + let Result:! Type = Self; + fn DoAdd[me: Self](right: AddWith) -> Result; +} +``` + +There are a few reasons for this feature: + +- When overriding would be inappropriate. +- Matching the functionality of non-virtual methods in base classes, so + interfaces can be a replacement for inheritance. +- Potentially reduce dynamic dispatch when using the interface in a + [`DynPtr`](#dynamic-types). + +Note that this applies to associated entities, not interface parameters. + ## Future work ### Dynamic types @@ -4305,17 +4463,6 @@ In Swift, there are discussions about implementing this feature under the name [4](https://forums.swift.org/t/se-0244-opaque-result-types-reopened/22942), Swift is considering spelling this ` V` or `some Collection`. -### Interface defaults - -Rust supports specifying defaults for -[interface parameters](https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#default-generic-type-parameters-and-operator-overloading), -[methods](https://doc.rust-lang.org/book/ch10-02-traits.html#default-implementations), -[associated constants](https://doc.rust-lang.org/reference/items/associated-items.html#associated-constants-examples). -We should support this too. It is helpful for evolution, as well as reducing -boilerplate. Defaults address the gap between the minimum necessary for a type -to provide the desired functionality of an interface and the breadth of API that -user's desire. - ### Evolution There are a collection of use cases for making different changes to interfaces @@ -4401,3 +4548,4 @@ parameter, as opposed to an associated type, as in `N:! u32 where ___ >= 2`. - [#920: Generic parameterized impls (details 5)](https://github.com/carbon-language/carbon-lang/pull/920) - [#950: Generic details 6: remove facets](https://github.com/carbon-language/carbon-lang/pull/950) - [#983: Generic details 7: final impls](https://github.com/carbon-language/carbon-lang/pull/983) +- [#990: Generics details 8: interface default and final members](https://github.com/carbon-language/carbon-lang/pull/990) diff --git a/docs/design/lexical_conventions/words.md b/docs/design/lexical_conventions/words.md index d61e8899c5201..6b7c6fe32aed1 100644 --- a/docs/design/lexical_conventions/words.md +++ b/docs/design/lexical_conventions/words.md @@ -49,6 +49,7 @@ The following words are interpreted as keywords: - `else` - `extends` - `external` +- `final` - `fn` - `for` - `friend` diff --git a/proposals/p0990.md b/proposals/p0990.md new file mode 100644 index 0000000000000..5c02376ce029b --- /dev/null +++ b/proposals/p0990.md @@ -0,0 +1,155 @@ +# Generics details 8: interface default and final members + + + +[Pull request](https://github.com/carbon-language/carbon-lang/pull/990) + + + +## Table of contents + +- [Problem](#problem) +- [Background](#background) +- [Proposal](#proposal) +- [Rationale based on Carbon's goals](#rationale-based-on-carbons-goals) +- [Alternatives considered](#alternatives-considered) + - [Defaulting to less specialized impls](#defaulting-to-less-specialized-impls) + - [Allow default implementations of required interfaces](#allow-default-implementations-of-required-interfaces) + - [Don't support `final`](#dont-support-final) + + + +## Problem + +Rust has found that allowing interfaces to define default values for its +associated entities is valuable: + +- Helps with evolution by reducing the changes needed to add new members to an + interface. +- Reduces boilerplate when some value is more common than others. +- Addresses the gap between the minimum necessary for a type to provide the + desired functionality of an interface and the breadth of API that user's + desire. + +Carbon would benefit in the same ways. + +## Background + +Rust supports specifying defaults for +[methods](https://doc.rust-lang.org/book/ch10-02-traits.html#default-implementations), +[interface parameters](https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#default-generic-type-parameters-and-operator-overloading), +and +[associated constants](https://doc.rust-lang.org/reference/items/associated-items.html#associated-constants-examples). + +## Proposal + +This proposal defines both how defaults for interface members are specified in +Carbon code as well as final interface members in the +[generics details design doc](/docs/design/generics/details.md#interface-defaults). + +## Rationale based on Carbon's goals + +This proposal advances these goals of Carbon: + +- [Performance-critical software](/docs/project/goals.md#performance-critical-software): + Final members of interfaces can avoid some dynamic dispatch overhead. +- [Software and language evolution](/docs/project/goals.md#software-and-language-evolution): + Defaults simplify adding new members to an interface without having to + simultaneously update all impls of that interface. +- [Code that is easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write): + Defaults both reduce boilerplate, making code easier to read and write. + Marking interface members as `final` makes the code more predictable to + users of that member. + +## Alternatives considered + +### Defaulting to less specialized impls + +Rust has observed +([1](https://rust-lang.github.io/rfcs/1210-impl-specialization.html#default-impls), +[2](http://aturon.github.io/tech/2015/09/18/reuse/)) that interface defaults +could be generalized into a feature for reusing definitions between impls. This +would involve allowing more specific implementations to be incomplete and reuse +more general implementations for anything unspecified. + +However, +[they also observed](http://smallcultfollowing.com/babysteps/blog/2016/09/29/distinguishing-reuse-from-override/): + +> [To be sound,] if an impl A wants to reuse some items from impl B, then impl A +> must apply to a subset of impl B's types. ... This implies we will have to +> separate the concept of "when you can reuse" (which requires subset of types) +> from "when you can override" (which can be more general). + +This is a source of complexity that we don't want in Carbon. If we do eventually +support inheritance of implementation between impls in Carbon, it will do this +by explicitly identifying the impl being reused instead of having it be +determined by their specialization relationship. + +### Allow default implementations of required interfaces + +Here are the reasons we considered for not allowing interfaces to provide +default implementations of interfaces they require: + +- This feature would lead to incoherence unless types implementing + `TotalOrder` also must explicitly implement `PartialOrder`, possibly with an + empty definition. The problem arises since querying whether `PartialOrder` + is implemented for a type does not require that an implementation of + `TotalOrder` be visible. +- It would be unclear how to resolve the ambiguity of which default to use + when two different interfaces provide different defaults for a common + interface requirement. +- It would be ambiguous whether the required interface should be + [external](/docs/design/generics/terminology.md#external-impl) or + [internal](/docs/design/generics/terminology.md#internal-impl) unless + `PartialOrder` is implemented explicitly. +- There would be a lot of overlap between default impls and blanket impls. + Eliminating default impls keeps the language smaller and simpler. + +The rules for blanket impls already provide resolution of the questions about +coherence and priority and make it clear that the provided definition of the +required interface will be external. + +### Don't support `final` + +There are a few reasons to support `final` on associated entities in the +interface: + +- Clarity of intent when default methods are just to provide an expanded API + for the convenience of callers, reducing the uncertainty about what code is + called. +- Matches the functionality available to base classes in C++, namely + non-virtual functions. +- Could reduce the amount of dynamic dispatch needed when using an interface + in a `DynPtr`. + +The main counter-argument is that you could achieve something similar using a +`final` impl: + +``` +interface I { + fn F(); + final fn CallF() { F(); } +} +``` + +could be replaced by: + +``` +interface IImpl { + fn F(); +} +interface I { + extends IImpl; + fn CallF(); +} +final impl (T:! IImpl) as I { + fn CallF() { F(); } +} +``` + +This is both verbose and a bit awkward to use since you would need to +`impl as IImpl` but use `I` in constraints.