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