From f4e9063b976b665e578e37154d99f817e74eebc2 Mon Sep 17 00:00:00 2001 From: josh11b Date: Fri, 21 Jan 2022 14:10:20 -0800 Subject: [PATCH] Generics details 8: interface default and final members (#990) Allowing interfaces to define default values for its associated entities. This: - 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. As an alternative, final values can be provided instead, which can't be overridden, but are more predictable for users and may avoid dynamic dispatch overhead in some cases. Example: ``` // Interface parameter has a default of `Self` interface Add(Right:! Type = Self) { // `AddWith` *always* equals `Right` final let AddWith:! Type = Right; // `Result` has a default of `Self` let Result:! Type = Self; fn DoAdd[me: Self](right: Right) -> Result; } impl String as Add() { // Right == AddWith == Result == Self == String fn DoAdd[me: Self](right: Self) -> Self; } ``` Co-authored-by: Richard Smith --- docs/design/generics/details.md | 172 +++++++++++++++++++++-- docs/design/lexical_conventions/words.md | 1 + proposals/p0990.md | 155 ++++++++++++++++++++ 3 files changed, 316 insertions(+), 12 deletions(-) create mode 100644 proposals/p0990.md diff --git a/docs/design/generics/details.md b/docs/design/generics/details.md index 3afc52a28e376..39de9c5b21f4a 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 716339c9fd948..9fe6115ba37a5 100644 --- a/docs/design/lexical_conventions/words.md +++ b/docs/design/lexical_conventions/words.md @@ -50,6 +50,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.