From 31042482fc9c950b90449096d112af1c71e3474d Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Tue, 3 May 2022 15:37:45 -0700 Subject: [PATCH] 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 --- docs/design/expressions/arithmetic.md | 62 ++-- docs/design/expressions/as_expressions.md | 2 +- .../expressions/comparison_operators.md | 289 ++++++++++++++++-- proposals/p1178.md | 100 ++++++ 4 files changed, 400 insertions(+), 53 deletions(-) create mode 100644 proposals/p1178.md diff --git a/docs/design/expressions/arithmetic.md b/docs/design/expressions/arithmetic.md index b044cbd04075c..54d91eb2f3d15 100644 --- a/docs/design/expressions/arithmetic.md +++ b/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) diff --git a/docs/design/expressions/as_expressions.md b/docs/design/expressions/as_expressions.md index 0b37d9f752894..fb6ffc301f7ef 100644 --- a/docs/design/expressions/as_expressions.md +++ b/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*; diff --git a/docs/design/expressions/comparison_operators.md b/docs/design/expressions/comparison_operators.md index 8f905699a10ee..4836922bef1b0 100644 --- a/docs/design/expressions/comparison_operators.md +++ b/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) diff --git a/proposals/p1178.md b/proposals/p1178.md new file mode 100644 index 0000000000000..88e3340bae77a --- /dev/null +++ b/proposals/p1178.md @@ -0,0 +1,100 @@ +# Rework operator interfaces + + + +[Pull request](https://github.com/carbon-language/carbon-lang/pull/1178 + + + +## 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) + + + +## 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.