mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-10-04 22:02:52 +01:00
Also updates a few spots with some subtle aspects changed in `lambdas.md` and `namespace_cleanliness.md`. Assisted-by: Antigravity with Gemini
529 lines
19 KiB
Markdown
529 lines
19 KiB
Markdown
# Comparison operators
|
|
|
|
<!--
|
|
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
|
|
-->
|
|
|
|
<!-- toc -->
|
|
|
|
## Table of contents
|
|
|
|
- [Overview](#overview)
|
|
- [Details](#details)
|
|
- [Precedence](#precedence)
|
|
- [Associativity](#associativity)
|
|
- [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)
|
|
- [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)
|
|
- [References](#references)
|
|
|
|
<!-- tocstop -->
|
|
|
|
## Overview
|
|
|
|
Carbon provides equality and relational comparison operators, each with a
|
|
standard mathematical meaning:
|
|
|
|
| Category | Operator | Example | Mathematical meaning | Description |
|
|
| ---------- | -------- | -------- | -------------------- | -------------------------- |
|
|
| Equality | `==` | `x == y` | = | Equality or equal to |
|
|
| Equality | `!=` | `x != y` | ≠ | Inequality or not equal to |
|
|
| Relational | `<` | `x < y` | < | Less than |
|
|
| Relational | `<=` | `x <= y` | ≤ | Less than or equal to |
|
|
| Relational | `>` | `x > y` | > | Greater than |
|
|
| Relational | `>=` | `x >= y` | ≥ | Greater than or equal to |
|
|
|
|
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
|
|
|
|
The comparison operators are all at the same precedence level. This level is
|
|
lower than operators used to compute (non-`bool`) values, higher than the
|
|
[logical operators](logical_operators.md) `and` and `or`, and incomparable with
|
|
the precedence of `not`.
|
|
|
|
For example:
|
|
|
|
```carbon
|
|
// ✅ Valid: precedence provides order of evaluation.
|
|
if (n + m * 3 < n * n and 3 < m and m < 6) {
|
|
...
|
|
}
|
|
// The above is equivalent to:
|
|
if (((n + (m * 3)) < (n * n)) and ((3 < m) and (m < 6))) {
|
|
...
|
|
}
|
|
|
|
// ❌ Invalid due to ambiguity: `(not a) == b` or `not (a == b)`?
|
|
if (not a == b) {
|
|
...
|
|
}
|
|
// ❌ Invalid due to precedence: write `a == (not b)`.
|
|
if (a == not b) {
|
|
...
|
|
}
|
|
// ❌ Invalid due to precedence: write `not (f < 5.0)`.
|
|
if (not f < 5.0) {
|
|
....
|
|
}
|
|
```
|
|
|
|
### Associativity
|
|
|
|
The comparison operators are non-associative. For example:
|
|
|
|
```carbon
|
|
// ❌ Invalid: write `3 < m and m < 6`.
|
|
if (3 < m < 6) {
|
|
...
|
|
}
|
|
// ❌ Invalid: write `a == b and b == c`.
|
|
if (a == b == c) {
|
|
...
|
|
}
|
|
// ❌ Invalid: write `(m > 1) == (n > 1)`.
|
|
if (m > 1 == n > 1) {
|
|
...
|
|
}
|
|
```
|
|
|
|
### Built-in comparisons and implicit conversions
|
|
|
|
Built-in comparisons are permitted in three cases:
|
|
|
|
1. When both operands are of standard Carbon integer types (`Int(n)` or
|
|
`Unsigned(n)`).
|
|
2. When both operands are of standard Carbon floating-point types (`Float(n)`).
|
|
3. When one operand is of floating-point type and the other is of integer type,
|
|
if all values of the integer type can be exactly represented in the
|
|
floating-point type.
|
|
|
|
In each case, the result is the mathematically-correct answer. This applies even
|
|
when comparing `Int(n)` with `Unsigned(m)`.
|
|
|
|
For example:
|
|
|
|
```carbon
|
|
// ✅ Valid: Fits case #1. The value of `compared` is `true` because `a` is less
|
|
// than `b`, even though the result of a wrapping `i32` or `u32` comparison
|
|
// would be `false`.
|
|
fn Compare(a: i32, b: u32) -> bool { return a < b; }
|
|
let compared: bool = Compare(-1, 4_000_000_000);
|
|
|
|
// ❌ Invalid: Doesn't fit case #3 because `i64` values in general are not
|
|
// exactly representable in the type `f32`.
|
|
let float: f32 = 1.0e18;
|
|
let integer: i64 = 1_000_000_000_000_000_000;
|
|
let eq: bool = float == integer;
|
|
```
|
|
|
|
Comparisons involving integer and floating-point constants are not covered by
|
|
these rules and are [discussed separately](#comparisons-with-constants).
|
|
|
|
#### Consistency with implicit conversions
|
|
|
|
We support the following [implicit conversions](implicit_conversions.md):
|
|
|
|
- From `Int(n)` to `Int(m)` if `m > n`.
|
|
- From `Unsigned(n)` to `Int(m)` or `Unsigned(m)` if `m > n`.
|
|
- From `Float(n)` to `Float(m)` if `m > n`.
|
|
- From `Int(n)` to `Float(m)` if `Float(m)` can represent all values of
|
|
`Int(n)`.
|
|
|
|
These rules can be summarized as: a type `T` can be converted to `U` if every
|
|
value of type `T` is a value of type `U`.
|
|
|
|
Implicit conversions are also supported from certain kinds of integer and
|
|
floating-point constants to `Int(n)` and `Float(n)` types, if the constant can
|
|
be represented in the type.
|
|
|
|
All built-in comparisons can be viewed as performing implicit conversions on at
|
|
most one of the operands in order to reach a suitable pair of identical or very
|
|
similar types, and then performing a comparison on those types. The target types
|
|
for these implicit conversions are, for each suitable value `n`:
|
|
|
|
- `Int(n)` versus `Int(n)`
|
|
- `Unsigned(n)` versus `Unsigned(n)`
|
|
- `Int(n)` versus `Unsigned(n)`
|
|
- `Unsigned(n)` versus `Int(n)`
|
|
- `Float(n)` versus `Float(n)`
|
|
|
|
There will in general be multiple combinations of implicit conversions that will
|
|
lead to one of the above forms, but we will arrive at the same result regardless
|
|
of which is selected, because all comparisons are mathematically correct and all
|
|
implicit conversions are lossless. Implementations are expected to do whatever
|
|
is most efficient: for example, for `u16 < i32` it is likely that the best
|
|
choice would be to promote the `u16` to `i32`, not `u32`.
|
|
|
|
Because we only ever convert at most one operand, we never use an intermediate
|
|
type that is larger than both input types. For example, both `i32` and `f32` can
|
|
be implicitly converted to `f64`, but we do not permit comparisons between `i32`
|
|
and `f32` even though we could perform those comparisons in `f64`. If such
|
|
comparisons were permitted, the results could be surprising:
|
|
|
|
```carbon
|
|
// `i32` can exactly represent this value.
|
|
var integer: i32 = 2_000_000_001;
|
|
// This value is within the representable range for `f32`, but will be rounded
|
|
// to 2_000_000_000.0 due to the limited precision of `f32`.
|
|
var float: f32 = 2_000_000_001.0;
|
|
|
|
// ❌ Invalid: `f32` cannot exactly represent all values of `i32`.
|
|
if (integer == float) {
|
|
...
|
|
}
|
|
|
|
// ✅ Valid: An explicit cast to `f64` on either side makes the code valid, but
|
|
// will compare unequal because `float` was rounded to 2_000_000_000.0
|
|
// but `integer` will convert to exactly 2_000_000_001.0.
|
|
if (integer == float as f64) {
|
|
...
|
|
}
|
|
if (integer as f64 == float) {
|
|
...
|
|
}
|
|
```
|
|
|
|
The two kinds of mixed-type comparison may be
|
|
[less efficient](/proposals/p0702.md#performance) than the other kinds due to
|
|
the slightly wider domain.
|
|
|
|
Note that this approach diverges from C++, which would convert both operands to
|
|
a common type first, sometimes performing a lossy conversion potentially giving
|
|
an incorrect result, sometimes converting both operands, and sometimes using a
|
|
wider type than either of the operand types.
|
|
|
|
#### Comparisons with constants
|
|
|
|
We permit the following comparisons involving constants:
|
|
|
|
- A constant can be compared with a value of any type to which it can be
|
|
implicitly converted.
|
|
- Any two constants can be compared, even if there is no type that can
|
|
represent both.
|
|
|
|
As described in [implicit conversions](implicit_conversions.md#data-types),
|
|
integer constants can be implicitly converted to any integer or floating-point
|
|
type that can represent their value, and floating-point constants can be
|
|
implicitly converted to any floating-point type that can represent their value.
|
|
|
|
Note that this disallows comparisons between, for example, `i32` and an integer
|
|
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.
|
|
|
|
### 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 checked-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(self, u: U) -> bool;
|
|
default fn NotEqual(self, u: U) -> bool {
|
|
return not (self == u);
|
|
}
|
|
}
|
|
constraint Eq {
|
|
extend 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(self) -> String;
|
|
|
|
impl as Eq {
|
|
fn Equal(self, other: Self) -> bool {
|
|
return (self.drive, self.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(self) -> i32 { return self.value; }
|
|
}
|
|
impl i32 as ImplicitAs(MyInt);
|
|
impl like MyInt as EqWith(like MyInt) {
|
|
fn Equal(self, other: Self) -> bool {
|
|
return self.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.
|
|
|
|
```
|
|
impl like MyFloat as EqWith(like MyFloat) {
|
|
fn Equal(self: MyFloat, other: MyFloat) -> bool {
|
|
if (self.IsNaN() or other.IsNaN()) {
|
|
return false;
|
|
}
|
|
return self.Representation() == other.Representation();
|
|
}
|
|
fn NotEqual(self: MyFloat, other: MyFloat) -> bool {
|
|
if (self.IsNaN() or other.IsNaN()) {
|
|
return false;
|
|
}
|
|
return self.Representation() != other.Representation();
|
|
}
|
|
}
|
|
```
|
|
|
|
Heterogeneous comparisons must be defined both ways around:
|
|
|
|
```
|
|
impl like MyInt as EqWith(like MyFloat);
|
|
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(self, u: U) -> Ordering;
|
|
default fn Less(self, u: U) -> bool {
|
|
return self.Compare(u) == Ordering.Less;
|
|
}
|
|
default fn LessOrEquivalent(self, u: U) -> bool {
|
|
let c: Ordering = self.Compare(u);
|
|
return c == Ordering.Less or c == Ordering.Equivalent;
|
|
}
|
|
default fn Greater(self, u: U) -> bool {
|
|
return self.Compare(u) == Ordering.Greater;
|
|
}
|
|
default fn GreaterOrEquivalent(self, u: U) -> bool {
|
|
let c: Ordering = self.Compare(u);
|
|
return c == Ordering.Greater or c == Ordering.Equivalent;
|
|
}
|
|
}
|
|
constraint Ordered {
|
|
extend OrderedWith(Self);
|
|
}
|
|
|
|
// Ordering.Less < Ordering.Equivalent < Ordering.Greater.
|
|
// Ordering.Incomparable is incomparable with all three.
|
|
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(self) -> i32 { return self.width * self.height; }
|
|
|
|
// Widgets are normally ordered by size.
|
|
impl as Ordered {
|
|
fn Compare(self, other: Self) -> Ordering {
|
|
return self.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);
|
|
}
|
|
impl like MyInt as OrderedWith(like MyFloat);
|
|
impl like MyFloat as OrderedWith(like MyInt) {
|
|
fn Compare(self, other: Self) -> Ordering {
|
|
return Reverse(other.(OrderedWith(Self).Compare)(self));
|
|
}
|
|
}
|
|
```
|
|
|
|
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 impls OrderedWith(U)` and
|
|
`U impls 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 checked 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
|
|
|
|
In addition to being defined for standard Carbon numeric types, equality and
|
|
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)
|
|
|
|
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
|
|
equality comparisons and relational comparisons if and only if choice types do
|
|
in general. That decision is left to a future proposal.
|
|
|
|
## Alternatives considered
|
|
|
|
- [Alternative symbols](/proposals/p0702.md#alternative-symbols)
|
|
- [Chained comparisons](/proposals/p0702.md#chained-comparisons-1)
|
|
- [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)
|