Jelajahi Sumber

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>
Richard Smith 4 tahun lalu
induk
melakukan
4a8ca9cd0f

+ 31 - 31
docs/design/expressions/arithmetic.md

@@ -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
docs/design/expressions/as_expressions.md

@@ -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
docs/design/expressions/comparison_operators.md

@@ -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
+
+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.
+
+#### Equality
+
+Comparison operators can be provided for user-defined types by implementing the
+`EqWith` and `OrderedWith` interfaces.
+
+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
 
-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:
+The `OrderedWith` interface is used to define the semantics of the `<`, `<=`,
+`>`, and `>=` operators for a given pair of types.
 
--   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.
+```
+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)`.
 
--   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.
+For example:
 
--   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.
+```
+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 - 0
proposals/p1178.md

@@ -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.