Rework operator interfaces (#1178)

Add concrete design for interfaces for comparison.

Rename interfaces for arithmetic following current thinking in #1058.

Update rules for mixed-type comparisons for data classes following #710.

Co-authored-by: Chandler Carruth <chandlerc@gmail.com>
This commit is contained in:
Richard Smith
2022-05-03 15:37:45 -07:00
committed by GitHub
co-authored by Chandler Carruth
parent f42ca044cb
commit 4a8ca9cd0f
4 changed files with 400 additions and 53 deletions
+31 -31
View File
@@ -182,77 +182,75 @@ following family of interfaces:
```
// Unary `-`.
interface Negatable {
interface Negate {
let Result:! Type = Self;
fn Negate[me: Self]() -> Result;
fn Op[me: Self]() -> Result;
}
```
```
// Binary `+`.
interface AddableWith(U:! Type) {
interface AddWith(U:! Type) {
let Result:! Type = Self;
fn Add[me: Self](other: U) -> Result;
fn Op[me: Self](other: U) -> Result;
}
constraint Addable {
extends AddableWith(Self) where .Result = Self;
constraint Add {
extends AddWith(Self) where .Result = Self;
}
```
```
// Binary `-`.
interface SubtractableWith(U:! Type) {
interface SubWith(U:! Type) {
let Result:! Type = Self;
fn Subtract[me: Self](other: U) -> Result;
fn Op[me: Self](other: U) -> Result;
}
constraint Subtractable {
extends SubtractableWith(Self) where .Result = Self;
constraint Sub {
extends SubWith(Self) where .Result = Self;
}
```
```
// Binary `*`.
interface MultipliableWith(U:! Type) {
interface MulWith(U:! Type) {
let Result:! Type = Self;
fn Multiply[me: Self](other: U) -> Result;
fn Op[me: Self](other: U) -> Result;
}
constraint Multipliable {
extends MultipliableWith(Self) where .Result = Self;
constraint Mul {
extends MulWith(Self) where .Result = Self;
}
```
```
// Binary `/`.
interface DividableWith(U:! Type) {
interface DivWith(U:! Type) {
let Result:! Type = Self;
fn Divide[me: Self](other: U) -> Result;
fn Op[me: Self](other: U) -> Result;
}
constraint Dividable {
extends DividableWith(Self) where .Result = Self;
constraint Div {
extends DivWith(Self) where .Result = Self;
}
```
```
// Binary `%`.
interface ModuloWith(U:! Type) {
interface ModWith(U:! Type) {
let Result:! Type = Self;
fn Mod[me: Self](other: U) -> Result;
fn Op[me: Self](other: U) -> Result;
}
constraint Modulo {
extends ModuloWith(Self) where .Result = Self;
constraint Mod {
extends ModWith(Self) where .Result = Self;
}
```
Given `x: T` and `y: U`:
- The expression `-x` is rewritten to `x.(Negatable.Negate)()`.
- The expression `x + y` is rewritten to `x.(AddableWith(U).Add)(y)`.
- The expression `x - y` is rewritten to
`x.(SubtractableWith(U).Subtract)(y)`.
- The expression `x * y` is rewritten to
`x.(MultipliableWith(U).Multiply)(y)`.
- The expression `x / y` is rewritten to `x.(DividableWith(U).Divide)(y)`.
- The expression `x % y` is rewritten to `x.(ModuloWith(U).Mod)(y)`.
- The expression `-x` is rewritten to `x.(Negate.Op)()`.
- The expression `x + y` is rewritten to `x.(AddWith(U).Op)(y)`.
- The expression `x - y` is rewritten to `x.(SubWith(U).Op)(y)`.
- The expression `x * y` is rewritten to `x.(MulWith(U).Op)(y)`.
- The expression `x / y` is rewritten to `x.(DivWith(U).Op)(y)`.
- The expression `x % y` is rewritten to `x.(ModWith(U).Op)(y)`.
Implementations of these interfaces are provided for built-in types as necessary
to give the semantics described above.
@@ -278,4 +276,6 @@ to give the semantics described above.
## References
- Proposal
[#1083: arithmetic](https://github.com/carbon-language/carbon-lang/pull/1083).
[#1083: Arithmetic](https://github.com/carbon-language/carbon-lang/pull/1083)
- Proposal
[#1178: Rework operator interfaces](https://github.com/carbon-language/carbon-lang/pull/1178)
+1 -1
View File
@@ -102,7 +102,7 @@ unordered with respect to binary arithmetic, bitwise operators, and unary `not`.
```
// OK
var x: i32* as Comparable;
var x: i32* as Eq;
// OK, `x as (U*)` not `(x as U)*`.
var y: auto = x as U*;
+268 -21
View File
@@ -17,7 +17,11 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
- [Built-in comparisons and implicit conversions](#built-in-comparisons-and-implicit-conversions)
- [Consistency with implicit conversions](#consistency-with-implicit-conversions)
- [Comparisons with constants](#comparisons-with-constants)
- [Overloading](#overloading)
- [Extensibility](#extensibility)
- [Equality](#equality)
- [Ordering](#ordering)
- [Compatibility of equality and ordering](#compatibility-of-equality-and-ordering)
- [Custom result types](#custom-result-types)
- [Default implementations for basic types](#default-implementations-for-basic-types)
- [Open questions](#open-questions)
- [Alternatives considered](#alternatives-considered)
@@ -43,6 +47,15 @@ Comparison operators all return a `bool`; they evaluate to `true` when the
indicated comparison is true. All comparison operators are infix binary
operators.
These operators have predefined meanings for some of Carbon's
[built-in types](#built-in-comparisons-and-implicit-conversions), as well as for
simple ["data" types](#default-implementations-for-basic-types) like structs and
tuples.
User-defined types can define the meaning of these operations by
[implementing an interface](#extensibility) provided as part of the Carbon
standard library.
## Details
### Precedence
@@ -222,29 +235,245 @@ literal that cannot be represented in `i32`. Such comparisons would always be
tautological. This decision should be revisited if it proves problematic in
practice, for example in templated code where the literal is sometimes in range.
### Overloading
### Extensibility
Separate interfaces will be provided to permit overloading equality and
relational comparisons. The exact design of those interfaces is left to a future
proposal. As non-binding design guidance for such a proposal:
User-defined types can extend the behavior of the comparison operators by
implementing interfaces. In this section, various properties are specified that
such implementations "should" satisfy. These properties are not enforced in
general, but the standard library might detect violations of some of them in
some circumstances. These properties may be assumed by generic code, resulting
in unexpected behavior if they are violated.
- The interface for equality comparisons should primarily provide the ability
to override the behavior of `==`. The `!=` operator can optionally also be
overridden, with a default implementation that returns `not (a == b)`. This
conversation was marked as resolved by chandlerc Show conversation
Overriding `!=` separately from `==` is expected to be used to support
floating-point NaN comparisons and for C++ interoperability.
#### Equality
- The interface for relational comparisons should primarily provide the
ability to specify a three-way comparison operator. The individual
relational comparison operators can optionally be overridden separately,
with a default implementation in terms of the three-way comparison operator.
This facility is expected to be used primarily to support C++
interoperability.
Comparison operators can be provided for user-defined types by implementing the
`EqWith` and `OrderedWith` interfaces.
- Overloaded comparison operators may wish to produce a type other than
`bool`, for uses such as a vector comparison producing a vector of `bool`
values. We should decide whether we wish to support such uses.
The `EqWith` interface is used to define the semantics of the `==` and `!=`
operators for a given pair of types:
```
interface EqWith(U:! Type) {
fn Equal[me: Self](u: U) -> bool;
default fn NotEqual[me: Self](u: U) -> bool {
return not (me == u);
}
}
constraint Eq {
extends EqWith(Self);
}
```
Given `x: T` and `y: U`:
- The expression `x == y` calls `x.(EqWith(U).Equal)(y)`.
- The expression `x != y` calls `x.(EqWith(U).NotEqual)(y)`.
```
class Path {
private var drive: String;
private var path: String;
private fn CanonicalPath[me: Self]() -> String;
external impl as Eq {
fn Equal[me: Self](other: Self) -> bool {
return (me.drive, me.CanonicalPath()) ==
(other.drive, other.CanonicalPath());
}
}
}
```
The `EqWith` overload is selected without considering possible implicit
conversions. To permit implicit conversions in the operands of an `==` overload,
the
[`like` operator](/docs/design/generics/details.md#like-operator-for-implicit-conversions)
can be used:
```
class MyInt {
var value: i32;
fn Value[me: Self]() -> i32 { return me.value; }
}
external impl i32 as ImplicitAs(MyInt);
external impl like MyInt as EqWith(like MyInt) {
fn Equal[me: Self](other: Self) -> bool {
return me.Value() == other.Value();
}
}
fn CompareBothWays(a: MyInt, b: i32, c: MyInt) -> bool {
// OK, calls above implementation three times.
return a == a and a != b and b == c;
}
```
The behavior of `NotEqual` can be overridden separately from the behavior of
`Equal` to support cases like floating-point NaN values, where two values can
compare neither equal nor not-equal, and thus both functions would return
`false`. However, an implementation of `EqWith` should _not_ allow both `Equal`
and `NotEqual` to return `true` for the same pair of values. Additionally, these
operations should have no observable side-effects.
```
external impl like MyFloat as EqWith(like MyFloat) {
fn Equal[me: MyFloat](other: MyFloat) -> bool {
if (me.IsNaN() or other.IsNaN()) {
return false;
}
return me.Representation() == other.Representation();
}
fn NotEqual[me: MyFloat](other: MyFloat) -> bool {
if (me.IsNaN() or other.IsNaN()) {
return false;
}
return me.Representation() != other.Representation();
}
}
```
Heterogeneous comparisons must be defined both ways around:
```
external impl like MyInt as EqWith(like MyFloat);
external impl like MyFloat as EqWith(like MyInt);
```
**TODO:** Add an adapter to the standard library to make it easy to define the
reverse comparison.
#### Ordering
The `OrderedWith` interface is used to define the semantics of the `<`, `<=`,
`>`, and `>=` operators for a given pair of types.
```
choice Ordering {
Less,
Equivalent,
Greater,
Incomparable
}
interface OrderedWith(U:! Type) {
fn Compare[me: Self](u: U) -> Ordering;
default fn Less[me: Self](u: U) -> bool {
return me.Compare(u) == Ordering.Less;
}
default fn LessOrEquivalent[me: Self](u: U) -> bool {
let c: Ordering = me.Compare(u);
return c == Ordering.Less or c == Ordering.Equivalent;
}
default fn Greater[me: Self](u: U) -> bool {
return me.Compare(u) == Ordering.Greater;
}
default fn GreaterOrEquivalent[me: Self](u: U) -> bool {
let c: Ordering = me.Compare(u);
return c == Ordering.Greater or c == Ordering.Equivalent;
}
}
constraint Ordered {
extends OrderedWith(Self);
}
// Ordering.Less < Ordering.Equivalent < Ordering.Greater.
// Ordering.Incomparable is incomparable with all three.
external impl Ordering as Ordered;
```
**TODO:** Revise the above when we have a concrete design for enumerated types.
Given `x: T` and `y: U`:
- The expression `x < y` calls `x.(OrderedWith(U).Less)(y)`.
- The expression `x <= y` calls `x.(OrderedWith(U).LessOrEquivalent)(y)`.
- The expression `x > y` calls `x.(OrderedWith(U).Greater)(y)`.
- The expression `x >= y` calls `x.(OrderedWith(U).GreaterOrEquivalent)(y)`.
For example:
```
class MyWidget {
var width: i32;
var height: i32;
fn Size[me: Self]() -> i32 { return me.width * me.height; }
// Widgets are normally ordered by size.
external impl as Ordered {
fn Compare[me: Self](other: Self) -> Ordering {
return me.Size().(Ordered.Compare)(other.Size());
}
}
}
fn F(a: MyWidget, b: MyWidget) -> bool {
return a <= b;
}
```
As for `EqWith`, the
[`like` operator](/docs/design/generics/details.md#like-operator-for-implicit-conversions)
can be used to permit implicit conversions when invoking a comparison, and
heterogeneous comparisons must be defined both ways around:
```
fn ReverseOrdering(o: Ordering) -> Ordering {
return Ordering.Equivalent.(Ordered.Compare)(o);
}
external impl like MyInt as OrderedWith(like MyFloat);
external impl like MyFloat as OrderedWith(like MyInt) {
fn Compare[me: Self](other: Self) -> Ordering {
return Reverse(other.(OrderedWith(Self).Compare)(me));
}
}
```
The default implementations of `Less`, `LessOrEquivalent`, `Greater`, and
`GreaterOrEquivalent` can be overridden if a more efficient version can be
implemented. The behaviors of such overrides should follow those of the above
default implementations, and the members of an `OrderedWith` implementation
should have no observable side-effects.
`OrderedWith` implementations should be _transitive_. That is, given `V:! Type`,
`U:! OrderedWith(V)`, `T:! OrderedWith(U) & OrderedWith(V)`, `a: T`, `b: U`,
`c: V`, then:
- If `a <= b` and `b <= c` then `a <= c`, and moreover if either `a < b` or
`b < c` then `a < c`.
- If `a >= b` and `b >= c` then `a >= c`, and moreover if either `a > b` or
`b > c` then `a > c`.
- If `a` and `b` are equivalent, then `a.Compare(c) == b.Compare(c)`.
Similarly, if `b` and `c` are equivalent, then
`a.Compare(b) == a.Compare(c)`.
`OrderedWith` implementations should also be _consistent under reversal_. That
is, given types `T` and `U` where `T is OrderedWith(U)` and
`U is OrderedWith(T)`, and values `a: T` and `b: U`:
- If `a.(OrderedWith.Compare)(b)` is `Ordering.Greater`, then
`b.(OrderedWith.Compare)(a)` is `Ordering.Less`, and the other way around.
- Otherwise, `a.(OrderedWith.Compare)(b)` returns the same value as
`b.(OrderedWith.Compare)(a)`.
There is no expectation that an `Ordered` implementation be a total order, a
weak order, or a partial order, and in particular the implementation for
floating-point types is none of these because NaN values do not compare less
than or equivalent to themselves.
**TODO:** The standard library should provide a way to specify that an ordering
is a weak, partial, or total ordering, and a way to request such an ordering in
a generic.
#### Compatibility of equality and ordering
There is no requirement that a pair of types that implements `OrderedWith` also
implements `EqWith`. If a pair of types does implement both, however, the
equality relation provided by `x.(EqWith.Equal)(y)` should be a refinement of
the equivalence relation provided by
`x.(OrderedWith.Compare)(y) == Ordering.Equivalent`.
#### Custom result types
**TODO:** Support a lower-level extensibility mechanism that allows a result
type other than `bool`.
### Default implementations for basic types
@@ -253,12 +482,27 @@ relational comparisons are also defined for all "data" types:
- [Tuples](../tuples.md)
- [Struct types](../classes.md#struct-types)
- [Classes implementing an interface that identifies them as data classes.](../classes.md#interfaces-implemented-for-data-classes)
- [Classes implementing an interface that identifies them as data classes](../classes.md#interfaces-implemented-for-data-classes)
Relational comparisons for these types provide a lexicographical ordering. In
each case, the comparison is only available if it is supported by all element
types.
Because implicit conversions between data classes can reorder fields, the
implementations for data classes do not permit implicit conversions on their
arguments in general. Instead:
- Equality comparisons are permitted between any two data classes that have
the same _unordered set_ of field names, if each corresponding pair of
fields has an `EqWith` implementation. Fields are compared in the order they
appear in the left-hand operand.
- Relational comparisons are permitted between any two data classes that have
the same _ordered sequence_ of field names, if each corresponding pair of
fields has an `OrderedWith` implementation. Fields are compared in order.
Comparisons between tuples permit implicit conversions for either operand, but
not both.
## Open questions
The `bool` type should be treated as a choice type, and so should support
@@ -272,10 +516,13 @@ in general. That decision is left to a future proposal.
- [Convert operands like C++](/proposals/p0702.md#convert-operands-like-c)
- [Provide a three-way comparison operator](/proposals/p0702.md#provide-a-three-way-comparison-operator)
- [Allow comparisons as the operand of `not`](/proposals/p0702.md#allow-comparisons-as-the-operand-of-not)
- [Rename `OrderedWith` to `ComparableWith`](/proposals/p1178.md#use-comparablewith-instead-of-orderedwith)
## References
- Proposal
[#702: Comparison operators](https://github.com/carbon-language/carbon-lang/pull/702)
- Proposal
[#1178: Rework operator interfaces](https://github.com/carbon-language/carbon-lang/pull/1178)
- Issue
[#710: Default comparison for data classes](https://github.com/carbon-language/carbon-lang/issues/710)
+100
View File
@@ -0,0 +1,100 @@
# Rework operator interfaces
<!--
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/1178
<!-- toc -->
## Table of contents
- [Problem](#problem)
- [Background](#background)
- [Proposal](#proposal)
- [Details](#details)
- [Rationale](#rationale)
- [Alternatives considered](#alternatives-considered)
- [Use `ComparableWith` instead of `OrderedWith`](#use-comparablewith-instead-of-orderedwith)
<!-- tocstop -->
## Problem
Our operator interface names need to be updated to match the decision in
[#1058](https://github.com/carbon-language/carbon-lang/issues/1058). Further, we
are missing a description of the interfaces used to overload comparison
operators, and the rules are not up to date with the decision in
[#710](https://github.com/carbon-language/carbon-lang/issues/710).
## Background
See the two leads issues for background and discussion of options.
## Proposal
See changes to the design.
## Details
Beyond establishing names for interfaces, this proposal also establishes:
- We will have high-level interfaces for equality and relational comparison.
The equality interface provides both `==` and `!=`. The relational
comparison interface provides all of `<`, `<=`, `>`, and `>=`.
- Following the convention established for arithmetic operators, we provide
both a heterogeneous comparison interface and a homogeneous constraint. For
example, `T is EqWith(T)` is equivalent to `T is Eq`.
- The high-level interfaces always return `bool`.
- The high-level interfaces have expected semantics associated with them.
It is intended that we also provide low-level interfaces, to directly control
individual operators and to allow a result type other than `bool`. These are not
included in this proposal, as it's not yet clear how they should be specified,
and it's more important to get the high-level interfaces decided at this point.
## Rationale
- [Language tools and ecosystem](/docs/project/goals.md#language-tools-and-ecosystem)
- High-level semantics allow tools to reason about the intended meaning of
Carbon code. For example, a tool could statically or dynamically
determine that an implementation of `Ordered` doesn't satisfy the
expected rules and produce a warning.
- [Performance-critical software](/docs/project/goals.md#performance-critical-software)
- We expect `==` and ordering to be customized separately, in order to
avoid cases where a suboptimal `==` is constructed in terms of an
ordering. See
[C++ committee paper P1190R0](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1190r0.html)
for details on the problem.
- [Code that is easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write)
- Combining all comparison operators of the same kind -- equality or
relational -- into a single interface makes it both easier to implement
them and easier to write a generic constraint for them. This approach is
also expected to be easy to teach, with the low-level interfaces only
explained to a more advanced audience.
- [Practical safety and testing mechanisms](/docs/project/goals.md#practical-safety-and-testing-mechanisms)
- While there are rules for the comparison interfaces, violating those
rules does not result in immediate unbounded undefined behavior.
However, implementations should still attempt to detect violations of
these rules and report them where that is feasible.
- [Interoperability with and migration from existing C++ code](/docs/project/goals.md#interoperability-with-and-migration-from-existing-c-code)
- The intent to provide a low-level interface for individual operators is
directly motivated by the desire to provide strong interoperability with
operators defined in C++. While this functionality is not part of this
proposal, it's expected to follow once the interactions with generics
are worked out.
## Alternatives considered
### Use `ComparableWith` instead of `OrderedWith`
We could use the term "comparable" for relational comparisons instead of
"ordered". There is existing practice for both: for example, Rust and Haskell
use `Ord`, and Swift uses `Comparable`.
The main argument for using "ordered" instead of "comparable" is that `==` and
`!=` are also a form of comparison but aren't part of `OrderedWith`, and the
word "ordered" distinguishes relational comparison from equality comparison.