Generic details 11: operator overloading (#1144)

Operators rewrite to calls of specific operator interface functions, so you overload an operator for a type by implementing an interface for it. There is a `like` operator for defining a set if implementations for supporting implicit conversions more conveniently.

Co-authored-by: Geoff Romer <gromer@google.com>
Co-authored-by: Richard Smith <richard@metafoo.co.uk>
This commit is contained in:
josh11b
2022-04-08 14:51:49 -07:00
committed by GitHub
co-authored by Geoff Romer Richard Smith
parent 9d0f55a4b5
commit db66a4350e
5 changed files with 536 additions and 16 deletions
+12 -8
View File
@@ -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
+347 -6
View File
@@ -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)
+19 -2
View File
@@ -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
+1
View File
@@ -61,6 +61,7 @@ The following words are interpreted as keywords:
- `is`
- `let`
- `library`
- `like`
- `match`
- `namespace`
- `not`
+157
View File
@@ -0,0 +1,157 @@
# Generic details 11: operator overloading
<!--
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/1144)
<!-- toc -->
## 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)
<!-- tocstop -->
## 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.