Shortcomings of D-Style Fused Operator Overloading
Quirin Schroll
qs.il.paperinik at gmail.com
Fri Jul 7 13:28:23 UTC 2023
**TL;DR: Getting orderings right in a programming language is
hard.**
On Friday, 7 July 2023 at 09:18:38 UTC, Ahmet Sait wrote:
> […]
> Exercise: Write an `opCmp()` method that implements the rules
> above for `struct Interval { int lower, upper; }`. (Hint: It's
> not possible.)
One can’t because D’s philosophy behind `opCmp` is a total
ordering and that `a <= b` is the same as `a < b || a == b`
(which both isn’t the case for your intervals).
The total ordering part can be stretched to a partial ordering by
making `opCmp` return `float` and use `float.nan` to indicate
that the elements are unrelated.
> […]
>
> So what do you guys think? Would you say distinct operators are
> better?
Yes, 100%. Orderings cannot really be done using D’s approach
with `opXXX`. Ideally, the programmer provides minimal details
about the ordering and the compiler generates the rest so that
ordering is consistent. As an example, C++ up until C++20
requires *you* to write both `==` and `!=`. In almost every
circumstance, one would define `!=` in terms of `==` completely
mechanical. I see no reason why `!=` should even be overloadable.
Maybe there are examples where `a != b` is not the same as `!(a
== b)`, I’m open enough to accept that, even though I’m not
creative enough to find something where this is reasonable. C++20
also added the `<=>` operator to allow for D’s approach because
in a lot of cases, it serves the programmer well.
I agree with you that `opCmp` is simplistic design; partial
orderings feel like a hack. I’ve thought about orderings a lot
and I’m glad you’ve exposed me to this, because now I need to
re-consider what the most general properties of ordering
relations could be. However, I think that your ordering relations
make little sense.
What properties should we minimally expect of `==`, `<` and `<=`,
`>`, and `>=` to be reasonable?
1. Rewrites
* `a != b` is the same as `!(a == b)`, therefore `!=` should
not be user-definable, `==` suffices.
* `a > b` is the same as `b < a`, therefore `>` should not be
user-definable, `<` suffices.
* `a >= b` is the same as `b <= a`, therefore `>=` should not
be user-definable, `<=` suffices.
2. Individual properties
* `<`, `<=`, and `==` are
[transitive](https://en.wikipedia.org/wiki/Transitive_relation):
E.g. for `<`: If `a < b` and `b < c` then `a < c`.
* `<` is totally irreflexive: `a < a` is always false.
* `<` is asymmetric: If `a < b`, then `b < a` is false.
* `==` is symmetrical: `a == b` is the same as `b == a`.
* `<=` is partially reflexive: For any `a` and `b`, if `a <=
b`, then `a <= a` and `b <= b`.
* `==` is partially reflexive as a consequence from
transitivity and symmetry: If `a == b`, by symmetry, also `b ==
a`, thus by transitivity, `a == a` and `b == b`.
3. Interactions
* `a == b` and `a < b` are mutually exclusive; in particular:
* If `a == b`, then `a < b` is false. (This is a
strengthening of irreflexivity.)
* If `a < b`, then `a != b`.
* Because `==` is symmetrical, the same for `b < a`.
* If `a < b` or `a == b`, then `a <= b` (definition of
*less-or-equal*).
* If `a <= b`, then `a < b` or `a == b` (definition of
*less-or-equal*).
* If `a <= b` and `a >= b`, then `a == b` (Anti-symmetry).
The last two or three are debatable; I have not put them in
Rewrites intentionally. As presented, the intervals don’t adhere
to the last two. It’s not even clear what `==` on the intervals
would be. It looks like the intervals are supposed to represent
*any value in the bounds,* which means that – unless the lower
and bound is the same, i.e. the interval is a single number – `I
== I` should actually fail.
In the axioms I presented, reflexivity is partial because `==`
and `<=` on IEEE floats are not reflexive: `float.nan !=
float.nan`, but any value that is equal to any other value is
equal to itself. And, of course, the axioms are supposed to be
true for build-in types.
While partial in the sense that not all elements are included,
IEEE floats are partially totally ordered:
* We indeed have `a <= b` or `b <= a` for all `a` and `b` such
that `a == a` and `b == b`.
IEEE floats are also anti-symmetric, and in general, `==` should
be the partial equivalence relation induced by `<=`: `a == b` if
and only if `a <= b` and `a >= b`.
For IEEE floats, positive and negative zero are equivalent, `+0.0
== -0.0`, but they are not equal (in bit pattern, i.e. `+0.0 !is
-0.0` and behavior: `1/+0.0 != 1/-0.0`). As another example, the
not-a-number values are behaviorally the same, they’re only
technically distinct.
I’ve written an [answer on
StackOverflow](https://stackoverflow.com/a/71844213/3273130) to
the question *What is the difference between equality and
equivalence?* A TL;DR is: D’s `is` operator is (at least close
to) mathematical equality, whereas the `==` operator is merely
the default equivalence relation, which can be anything, and
(think of `x` and `y` as `ref` parameters) `&x == &y` in D
represents identity (any mutation of `x` is a mutation of `y`).
The C++ proposal
[P0515R0](https://open-std.org/JTC1/SC22/WG21/docs/papers/2017/p0515r0.pdf) is also a good read. It was rejected, but introduces relevant notions. Look at least at § 1.4.1 *Guidance: What we teach.*
More information about the Digitalmars-d
mailing list