diff --git a/docs/design/classes.md b/docs/design/classes.md index 4500242cc5e5..6231ad2594a6 100644 --- a/docs/design/classes.md +++ b/docs/design/classes.md @@ -56,13 +56,13 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - [Friends](#friends) - [Test friendship](#test-friendship) - [Access control for construction](#access-control-for-construction) + - [Operator overloading](#operator-overloading) - [Future work](#future-work) - [Struct literal shortcut](#struct-literal-shortcut) - [Optional named parameters](#optional-named-parameters) - [Field defaults for struct types](#field-defaults-for-struct-types) - [Destructuring in pattern matching](#destructuring-in-pattern-matching) - [Discussion](#discussion) - - [Operator overloading](#operator-overloading) - [Inheritance](#inheritance-1) - [Destructors](#destructors) - [C++ abstract base classes interoperating with object-safe interfaces](#c-abstract-base-classes-interoperating-with-object-safe-interfaces) @@ -1548,6 +1548,17 @@ if it has access to (write) all of its fields. even when it only has public fields. This will be resolved in question-for-leads issue [#803](https://github.com/carbon-language/carbon-lang/issues/803). +### Operator overloading + +Developers may define how standard Carbon operators, such as `+` and `/`, apply +to custom types by implementing the +[interface](generics/terminology.md#interface) that corresponds to that operator +for the types of the operands. See the +["operator overloading" section](generics/details.md#operator-overloading) of +the [generics design](generics/overview.md). The specific interface used for a +given operator may be found in the +[expressions design](/docs/design/expressions/README.md). + ## Future work This includes features that need to be designed, questions to answer, and a @@ -1636,13 +1647,6 @@ Some discussion on this topic has occurred in: [2](https://docs.google.com/document/d/1u6GORSkcgThMAiYKOqsgALcEviEtcghGb5TTVT-U-N0/edit) - ["match" in syntax choices doc](https://docs.google.com/document/d/1iuytei37LPg_tEd6xe-O6P_bpN7TIbEjNtFMLYW2Nno/edit#heading=h.y566d16ivoy2) -### Operator overloading - -This includes destructors, copy and move operations, as well as other Carbon -operators such as `+` and `/`. We expect types to implement these operations by -implementing corresponding interfaces, see -[the generics overview](generics/overview.md). - ### Inheritance #### Destructors diff --git a/docs/design/generics/details.md b/docs/design/generics/details.md index 1543ecccd61d..c603f121eee2 100644 --- a/docs/design/generics/details.md +++ b/docs/design/generics/details.md @@ -95,6 +95,9 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - [Interface members with definitions](#interface-members-with-definitions) - [Interface defaults](#interface-defaults) - [`final` members](#final-members) +- [Operator overloading](#operator-overloading) + - [Binary operators](#binary-operators) + - [`like` operator for implicit conversions](#like-operator-for-implicit-conversions) - [Future work](#future-work) - [Dynamic types](#dynamic-types) - [Runtime type parameters](#runtime-type-parameters) @@ -102,7 +105,6 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - [Abstract return types](#abstract-return-types) - [Evolution](#evolution) - [Testing](#testing) - - [Operator overloading](#operator-overloading) - [Impls with state](#impls-with-state) - [Generic associated types and higher-ranked types](#generic-associated-types-and-higher-ranked-types) - [Generic associated types](#generic-associated-types) @@ -4440,6 +4442,349 @@ There are a few reasons for this feature: Note that this applies to associated entities, not interface parameters. +## Operator overloading + +Operations are overloaded for a type by implementing an interface specific to +that interface for that type. For example, types implement the `Negatable` +interface to overload the unary `-` operator: + +``` +// Unary `-`. +interface Negatable { + let Result:! Type = Self; + fn Negate[me: Self]() -> Result; +} +``` + +Expressions using operators are rewritten into calls to these interface methods. +For example, `-x` would be rewritten to `x.(Negatable.Negate)()`. + +The interfaces and rewrites used for a given operator may be found in the +[expressions design](/docs/design/expressions/README.md). +[Question-for-leads issue #1058](https://github.com/carbon-language/carbon-lang/issues/1058) +defines the naming scheme for these interfaces. + +### Binary operators + +Binary operators will have an interface that is +[parameterized](#parameterized-interfaces) based on the second operand. For +example, to say a type may be converted to another type using an `as` +expression, implement the +[`As` interface](/docs/design/expressions/as_expressions.md#extensibility): + +``` +interface As(Dest:! Type) { + fn Convert[me: Self]() -> Dest; +} +``` + +The expression `x as U` is rewritten to `x.(As(U).Convert)()`. Note that the +parameterization of the interface means it can be implemented multiple times to +support multiple operand types. + +Unlike `as`, for most binary operators the interface's argument will be the +_type_ of the right-hand operand instead of its _value_. Consider an interface +for a binary operator like `*`: + +``` +// Binary `*`. +interface MultipliableWith(U:! Type) { + let Result:! Type = Self; + fn Multiply[me: Self](other: U) -> Result; +} +``` + +A use of binary `*` in source code will be rewritten to use this interface: + +``` +var left: Meters = ...; +var right: f64 = ...; +var result: auto = left * right; +// Equivalent to: +var equivalent: left.(MultipliableWith(f64).Result) + = left.(MultipliableWith(f64).Multiply)(right); +``` + +Note that if the types of the two operands are different, then swapping the +order of the operands will result in a different implementation being selected. +It is up to the developer to make those consistent when that is appropriate. The +standard library will provide [adapters](#adapting-types) for defining the +second implementation from the first, as in: + +``` +interface ComparableWith(RHS:! Type) { + fn Compare[me: Self](right: RHS) -> CompareResult; +} + +adapter ReverseComparison + (T:! Type, U:! ComparableWith(RHS)) for T { + impl as ComparableWith(U) { + fn Compare[me: Self](right: RHS) -> CompareResult { + return ReverseCompareResult(right.Compare(me)); + } + } +} + +external impl SongByTitle as ComparableWith(SongTitle); +external impl SongTitle as ComparableWith(SongByTitle) + = ReverseComparison(SongTitle, SongByTitle); +``` + +In some cases the reverse operation may not be defined. For example, a library +might support subtracting a vector from a point, but not the other way around. + +Further note that even if the reverse implementation exists, +[the impl prioritization rule](#prioritization-rule) might not pick it. For +example, if we have two types that support comparison with anything implementing +an interface that the other implements: + +``` +interface IntLike { + fn AsInt[me: Self]() -> i64; +} + +class EvenInt { ... } +external impl EvenInt as IntLike; +external impl EvenInt as ComparableWith(EvenInt); +// Allow `EvenInt` to be compared with anything that +// implements `IntLike`, in either order. +external impl [T:! IntLike] EvenInt as ComparableWith(T); +external impl [T:! IntLike] T as ComparableWith(EvenInt); + +class PositiveInt { ... } +external impl PositiveInt as IntLike; +external impl PositiveInt as ComparableWith(PositiveInt); +// Allow `PositiveInt` to be compared with anything that +// implements `IntLike`, in either order. +external impl [T:! IntLike] PositiveInt as ComparableWith(T); +external impl [T:! IntLike] T as ComparableWith(PositiveInt); +``` + +Then it will favor selecting the implementation based on the type of the +left-hand operand: + +``` +var even: EvenInt = ...; +var positive: PositiveInt = ...; +// Uses `EvenInt as ComparableWith(T)` impl +if (even < positive) { ... } +// Uses `PositiveInt as ComparableWith(T)` impl +if (positive > even) { ... } +``` + +### `like` operator for implicit conversions + +Because the type of the operands is directly used to select the implementation +to use, there are no automatic implicit conversions, unlike with function or +method calls. Given both a method and an interface implementation for +multiplying by a value of type `f64`: + +``` +class Meters { + fn Scale[me: Self](s: f64) -> Self; +} +// "Implementation One" +external impl Meters as MultipliableWith(f64) + where .Result = Meters { + fn Multiply[me: Self](other: f64) -> Result { + return me.Scale(other); + } +} +``` + +the method will work with any argument that can be implicitly converted to `f64` +but the operator overload will only work with values that have the specific type +of `f64`: + +``` +var height: Meters = ...; +var scale: f32 = 1.25; +// ✅ Allowed: `scale` implicitly converted +// from `f32` to `f64`. +var allowed: Meters = height.Scale(scale); +// ❌ Illegal: `Meters` doesn't implement +// `MultipliableWith(f32)`. +var illegal: Meters = height * scale; +``` + +The workaround is to define a parameterized implementation that performs the +conversion. The implementation is for types that implement the +[`ImplicitAs` interface](/docs/design/expressions/implicit_conversions.md#extensibility). + +``` +// "Implementation Two" +external impl [T:! ImplicitAs(f64)] + Meters as MultipliableWith(T) where .Result = Meters { + fn Multiply[me: Self](other: T) -> Result { + // Carbon will implicitly convert `other` from type + // `T` to `f64` to perform this call. + return me.(Meters.(MultipliableWith(f64).Multiply))(other); + } +} +// ✅ Allowed: uses `Meters as MultipliableWith(T)` impl +// with `T == f32` since `f32 is ImplicitAs(f64)`. +var now_allowed: Meters = height * scale; +``` + +Observe that the [prioritization rule](#prioritization-rule) will still prefer +the unparameterized impl when there is an exact match. + +To reduce the boilerplate needed to support these implicit conversions when +defining operator overloads, Carbon has the `like` operator. This operator can +only be used in the type or type-of-type part of an `impl` declaration, as part +of a forward declaration or definition, in a place of a type. + +``` +// Notice `f64` has been replaced by `like f64` +// compared to "implementation one" above. +external impl Meters as MultipliableWith(like f64) + where .Result = Meters { + fn Multiply[me: Self](other: f64) -> Result { + return me.Scale(other); + } +} +``` + +This `impl` definition actually defines two implementations. The first is the +same as this definition with `like f64` replaced by `f64`, giving something +equivalent to "implementation one". The second implementation replaces the +`like f64` with a parameter that ranges over types that can be implicitly +converted to `f64`, equivalent to "implementation two". + +In general, each `like` adds one additional impl. There is always the impl with +all of the `like` expressions replaced by their arguments with the definition +supplied in the source code. In addition, for each `like` expression, there is +an impl with it replaced by a new parameter. These additional impls will +delegate to the main impl, which will trigger implicit conversions according to +[Carbon's ordinary implicit conversion rules](/docs/design/expressions/implicit_conversions.md). +In this example, there are two uses of `like`, producing three implementations + +``` +external impl like Meters as MultipliableWith(like f64) + where .Result = Meters { + fn Multiply[me: Self](other: f64) -> Result { + return me.Scale(other); + } +} +``` + +is equivalent to "implementation one", "implementation two", and: + +``` +external impl [T:! ImplicitAs(Meters)] + T as MultipliableWith(f64) where .Result = Meters { + fn Multiply[me: Self](other: f64) -> Result { + // Will implicitly convert `me` to `Meters` in order to + // match the signature of this `Multiply` method. + return me.(Meters.(MultipliableWith(f64).Multiply))(other); + } +} +``` + +`like` may be used in forward declarations in a way analogous to impl +definitions. + +``` +external impl like Meters as MultipliableWith(like f64) + where .Result = Meters; +} +``` + +is equivalent to: + +``` +// All `like`s removed. Same as the declaration part of +// "implementation one", without the body of the definition. +external impl Meters as MultipliableWith(f64) + where .Result = Meters; + +// First `like` replaced with a wildcard. +external impl [T:! ImplicitAs(Meters)] + T as MultipliableWith(f64) where .Result = Meters; + +// Second `like` replaced with a wildcard. Same as the +// declaration part of "implementation two", without the +// body of the definition. +external impl [T:! ImplicitAs(f64)] + Meters as MultipliableWith(T) where .Result = Meters; +``` + +In addition, the generated impl definition for a `like` is implicitly injected +at the end of the (unique) source file in which the impl is first declared. That +is, it is injected in the API file if the impl is declared in an API file, and +in the sole impl file declaring the impl otherwise. This means an `impl` +declaration using `like` in an API file also makes the parameterized definition + +If one `impl` declaration uses `like`, other declarations must use `like` in the +same way to match. + +The `like` operator may be nested, as in: + +``` +external impl like Vector(like String) as Printable; +``` + +Which will generate implementations with declarations: + +``` +external impl Vector(String) as Printable; +external impl [T:! ImplicitAs(Vector(String))] T as Printable; +external impl [T:! ImplicitAs(String)] Vector(T) as Printable; +``` + +The generated implementations must be legal or the `like` is illegal. For +example, it must be legal to define those impls in this library by the +[orphan rule](#orphan-rule). In addition, the generated `impl` definitions must +only require implicit conversions that are guaranteed to exist. For example, +there existing an implicit conversion from `T` to `String` does not imply that +there is one from `Vector(T)` to `Vector(String)`, so the following use of +`like` is illegal: + +``` +// ❌ Illegal: Can't convert a value with type +// `Vector(T:! ImplicitAs(String))` +// to `Vector(String)` for `me` +// parameter of `Printable.Print`. +external impl Vector(like String) as Printable; +``` + +Since the additional implementation definitions are generated eagerly, these +errors will be reported in the file with the first declaration. + +The argument to `like` must either not mention any type parameters, or those +parameters must be able to be determined due to being repeated outside of the +`like` expression. + +``` +// ✅ Allowed: no parameters +external impl like Meters as Printable; + +// ❌ Illegal: No other way to determine `T` +external impl [T:! IntLike] like T as Printable; + +// ❌ Illegal: `T` being used in a `where` clause +// is insufficient. +external impl [T:! IntLike] like T + as MultipliableWith(i64) where .Result = T; + +// ❌ Illegal: `like` can't be used in a `where` +// clause. +external impl Meters as MultipliableWith(f64) + where .Result = like Meters; + +// ✅ Allowed: `T` can be determined by another +// part of the query. +external impl [T:! IntLike] like T + as MultipliableWith(T) where .Result = T; +external impl [T:! IntLike] T + as MultipliableWith(like T) where .Result = T; + +// ✅ Allowed: Only one `like` used at a time, so this +// is equivalent to the above two examples. +external impl [T:! IntLike] like T + as MultipliableWith(like T) where .Result = T; +``` + ## Future work ### Dynamic types @@ -4489,11 +4834,6 @@ supported and made safe. The idea is that you would write tests alongside an interface that validate the expected behavior of any type implementing that interface. -### Operator overloading - -We will need a story for defining how an operation is overloaded for a type by -implementing an interface for that type. - ### Impls with state A feature we might consider where an `impl` itself can have state. @@ -4570,3 +4910,4 @@ be included in the declaration as well. - [#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) - [#1013: Generics: Set associated constants using where constraints](https://github.com/carbon-language/carbon-lang/pull/1013) +- [#1144: Generic details 11: operator overloading](https://github.com/carbon-language/carbon-lang/pull/1144) diff --git a/docs/design/generics/overview.md b/docs/design/generics/overview.md index 9a7ac1b54884..44c7a5a205a8 100644 --- a/docs/design/generics/overview.md +++ b/docs/design/generics/overview.md @@ -35,6 +35,7 @@ pointers to other design documents that dive deeper into individual topics. - [Parameterized interfaces](#parameterized-interfaces) - [Constraints](#constraints) - [Parameterized impls](#parameterized-impls) + - [Operator overloading](#operator-overloading) - [Future work](#future-work) - [References](#references) @@ -627,14 +628,30 @@ library defining some name from its type structure. If a library defines multiple implementations with the same type structure, they must be listed in priority order in a prioritization block. +### Operator overloading + +To overload an operator, implement the corresponding interface from the standard +library. For example, to define how the unary `-` operator behaves for a type, +implement the `Negatable` interface for that type. The interfaces and rewrites +used for a given operator may be found in the +[expressions design](/docs/design/expressions/README.md). + +As a convenience, there is a shorcut for defining an implementation that +supports any type implicitly convertible to a specified type, using `like`: + +``` +// Support multiplying values of type `Distance` with +// values of type `f64` or any type implicitly +// convertible to `f64`. +external impl Distance as MultipliableWith(like f64) ... +``` + ## Future work - Support functions should have a way to accept types that types that vary at runtime. - You should have the ability to mark entities as `upcoming` or `deprecated` to support evolution. -- Types should be able to define overloads for operators by implementing - standard interfaces. - There should be a way to provide default implementations of methods in interfaces and other ways to reuse code across implementations. - There should be a way to define generic associated and higher-ranked/kinded diff --git a/docs/design/lexical_conventions/words.md b/docs/design/lexical_conventions/words.md index b32c765c3ecb..1d8b98a74be3 100644 --- a/docs/design/lexical_conventions/words.md +++ b/docs/design/lexical_conventions/words.md @@ -61,6 +61,7 @@ The following words are interpreted as keywords: - `is` - `let` - `library` +- `like` - `match` - `namespace` - `not` diff --git a/proposals/p1144.md b/proposals/p1144.md new file mode 100644 index 000000000000..dd2c3cb1774b --- /dev/null +++ b/proposals/p1144.md @@ -0,0 +1,157 @@ +# Generic details 11: operator overloading + + + +[Pull request](https://github.com/carbon-language/carbon-lang/pull/1144) + + + +## Table of contents + +- [Problem](#problem) +- [Background](#background) +- [Proposal](#proposal) +- [Rationale based on Carbon's goals](#rationale-based-on-carbons-goals) +- [Alternatives considered](#alternatives-considered) + - [Weak impls instead of adapters for reverse implementations](#weak-impls-instead-of-adapters-for-reverse-implementations) + - [Default impls instead of adapters for reverse implementations](#default-impls-instead-of-adapters-for-reverse-implementations) + - [Allow an impl declaration with `like` to match one without](#allow-an-impl-declaration-with-like-to-match-one-without) + - [Where are the impl definitions from `like` generated?](#where-are-the-impl-definitions-from-like-generated) + - [Support marking interfaces or their members as `external`](#support-marking-interfaces-or-their-members-as-external) + + + +## Problem + +C++ supports +[operator overloading](https://en.wikipedia.org/wiki/Operator_overloading), and +we would like Carbon to as well. This proposal is about the general problem, not +the specifics application to any particular operator. + +This proposal does not attempt to define a mechanism by which we can ensure that +`a < b` has the same value as `b > a`. + +## Background + +The generics feature is the +[single static open extension mechanism](/docs/project/principles/static_open_extension.md) +in Carbon, and so will be what we use operator overloading. We have already +started specifying the ability to extend or customize the behavior of operators +by implementing interfaces, as in these proposals: + +- [#820: Implicit conversions](https://github.com/carbon-language/carbon-lang/pull/820) +- [#845: as expressions](https://github.com/carbon-language/carbon-lang/pull/845) +- [#911: Conditional expressions](https://github.com/carbon-language/carbon-lang/pull/911) +- [#1083: Arithmetic expressions](https://github.com/carbon-language/carbon-lang/pull/1083) + +Proposal +[#702: Comparison operators](https://github.com/carbon-language/carbon-lang/pull/702) +specified +[using interfaces for overloading the comparison operators](p0702.md#overloading), +but did not pin down specifically what those interfaces are. + +## Proposal + +This proposal adds an +["Operator overloading" section](/docs/design/generics/details.md#operator-overloading) +to the [detailed design of generics](/docs/design/generics/details.md). + +## Rationale based on Carbon's goals + +This proposal advances Carbon's goals: + +- [Code that is easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write), + by making common constructs more concise, and allowing the syntax to more + closely mirror notation used math or the application domain. +- [Software and language evolution](/docs/project/goals.md#software-and-language-evolution), + since this allows standard types to implement operators using the same + mechanisms that user types do, this allows changes between what is built-in + versus provided in a library without user-visible impact. + +## Alternatives considered + +The current proposal requires the user to define a reverse implementation, and +recommends using an adapter to do that more conveniently. We also considered +approaches that would provide the reverse implementation more automatically. + +### Weak impls instead of adapters for reverse implementations + +We proposed +[weak impls](https://github.com/carbon-language/carbon-lang/pull/1027) as a way +of defining blanket impls for the reverse impl that did not introduce +[cycles](/docs/design/generics/details.md#acyclic-rule). We rejected that +approach due to giving the reverse implementation the wrong priority. This meant +that there were many situations where `a < b` and `b > a` would give different +answers. + +### Default impls instead of adapters for reverse implementations + +We then proposed +[default impls](https://github.com/carbon-language/carbon-lang/pull/1034) as a +way to define reverse implementations. These were rejected because they had a +lot of overlap with blanket impls, making it difficult to describe when to use +one over the other, and because they introduced a lot of complexity without +fully solving the priority problem. Most of the complexity was from the criteria +for determining whether the default implementation would be used. As +[noted](/docs/design/generics/details.md#binary-operators), the current proposal +still has some priority issues, but this way the relevant impls are visible in +the source which will hopefully make it clearer why it happens. + +The capability provided by default impls -- the ability to conveniently give +implementations of other interfaces -- may prove useful enough that we would +reconsider this decision in the future. + +### Allow an impl declaration with `like` to match one without + +We considered allowing an impl declared with `like` to match the equivalent +impls without `like`. The main concern was there would not be a canonical form +without `like`, particularly of how the newly introduced parameter would be +written. We thought we might say that the `like` declaration, since it omits a +spelling of the parameter, is allowed to match any spelling of the parameter. +However, there would still be a question of whether to use a deduced parameter, +as in `[T:! ImplicitAs(i64)] Vector(T)` or not as in +`Vector(T:! ImplicitAs(i64))`. We also considered the canonical form of +`Vector(_:! ImplicitAs(i64))` without naming the parameter. In the end, we +decided to start with a restrictive approach with the knowledge that we could +change once we gained experience. + +The main use case for allowing declarations in a different form, which may +motivate changes in the future, is to prioritize the different implementations +generated by the `like` shortcut separately in `match_first` blocks. + +This was discussed in +[open discussion on 2022-03-24](https://docs.google.com/document/d/1cRrhRrmaUf2hVi2lFcHsYo2j0jI6t9RGZoYjWhRxp14/edit?resourcekey=0-xWHBEZ8zIqnJiB4yfBSLfA#). + +### Where are the impl definitions from `like` generated? + +We considered whether the additional impl definitions would be generated with +the first declaration of an impl using `like` or with its definition. We +ultimately decided on the former approach for two reasons: + +- The generated impl definitions are parameterized even if the explicit + definition is not, and parameterized impl definitions may need to be in the + API file to allow separate compilation. +- This will make the code doing implicit conversions visible to callers, + allowing it to be inlined, matching how the caller does implicit conversions + for method calls. + +This was discussed in +[the #generics channel on Discord](https://discord.com/channels/655572317891461132/941071822756143115/962059164014739526). + +### Support marking interfaces or their members as `external` + +We +[discussed on 2022-03-28](https://docs.google.com/document/d/1cRrhRrmaUf2hVi2lFcHsYo2j0jI6t9RGZoYjWhRxp14/edit?resourcekey=0-xWHBEZ8zIqnJiB4yfBSLfA#heading=h.sk06n1ggoa3o) +the idea that operator interfaces might be marked `external`. This would either +mean that types would only be able to implement them using `external impl` or +that even if they were implemented internally, they would not add the names of +the interface members to the type. Alternatively, individual members might be +marked `external` to indicate that their names are not added to implementing +types, which might also be useful for making changes to an interface in a +compatible way. + +We were not sure if this feature was needed, so we left this as future work.