mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-10-05 16:31:04 +01:00
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:
committed by
GitHub
co-authored by
Geoff Romer
Richard Smith
parent
9d0f55a4b5
commit
db66a4350e
+12
-8
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -61,6 +61,7 @@ The following words are interpreted as keywords:
|
||||
- `is`
|
||||
- `let`
|
||||
- `library`
|
||||
- `like`
|
||||
- `match`
|
||||
- `namespace`
|
||||
- `not`
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user