Value object equality & floating-point values

- liangchenblue at gmail.com
Fri Feb 9 19:52:24 UTC 2024


Hi Dan and Remi,
In addition to Dan's reference pointer example, which I strongly agree
with, I wish to share another example that more closely aligns with the
existing double or float: OptionalInt.

OptionalInt has 2 fields: boolean isPresent and int value. However, not all
possible combinations of the 2 fields are usually used; like NaN has many
representations, the absent value has many representations (its value field
can take any int value).

When we have an OptionalInt(false, 0) and an OptionalInt(false, 1), should
we have == stand true for them? No, even though their behaviors are the
same otherwise.

"But OptionalInt's constructor prevents these values!" One might argue. We
wish that double had constructors that prevented creation of distinct NaNs,
too, as it is not a cartesian product of its components.

Even though we must live with this vestige of positive and negative zero
and many types of infinity and the special == operator logic for double, I
don't think it's a convincing argument for us to complicate the object ==
logic to support non-cartesian values in corner-case states to be the same
as their "normal" states. We now have constructors to normalize and
sanitize input values if you really love ==, so the OptionalInt scenario
never happens in practice; nothing prevents you from normalizing the double
values you write to your strict fields, and you can move on to use == as
you please.

Regards,
Chen Liang

On Fri, Feb 9, 2024 at 12:56 PM Dan Smith <daniel.smith at oracle.com> wrote:

> On Feb 9, 2024, at 10:13 AM, Remi Forax <forax at univ-mlv.fr> wrote:
>
> value class C {
>    private double d;
>    C(double d) { this.d = d; }
>    long bits() { return Double.doubleToRawLongBits(d); }
> }
>
> C c1 = new C(Double.longBitsToDouble(0x7ff0000000000001L));
> C c2 = new C(Double.longBitsToDouble(0x7ff0000000000002L));
> assert c1.bits() != c2.bits();
>
> Will this assert ever fail? Well, it depends on the JVM treats c1 and c2
> as belonging to the same equivalence class. If they are, it's allowed to
> substitute c1 for c2 at any time. I think it's pretty clear that would be a
> mistake.
>
>
> I do not compute that statement :)
>
> Why do you want users to care about the bitwise representation of NaN ?
> Both 0x7ff0000000000001L and 0x7ff0000000000002L represents NaN, if we
> print c1.d and c2.d both will print NaN, if we use c1.d or c2.d in numeric
> computation, they will both behave as NaN.
>
>
> To be very specific about this example, I think it's bad if the result of
> the 'c.bits()' method is nondeterministic, making the 'assert' result
> unpredictable.
>
> Two instances of C with representationally-equivalent state can produce
> different results from their 'c.bits()' method. So they aren't
> substitutable (per the "can substituted for one another without changing
> the result of the expression" definition). I would be uncomfortable with
> the JVM substituting one for the other whenever it wants to.
>
> Sure, the *native* operations on *double* almost never distinguish between
> different NaN encodings. But a *custom* operation on *a class that wraps a
> double* certainly can.
>
> (The example could be improved by doing a better job of illustrating that
> the double is private internal state, and that the operations exposed by
> the class need not look at all like floating-point operations, as far as
> the client of the class is concerned. All they know is they've got an
> object that is randomly producing nondeterministic results.)
>
> This, by itself, is not an argument for '==' being defined to use bitwise
> equivalence, but it is an argument for a well-defined concept of
> "substitutable value object" that is based on bitwise equivalence.
>
> Using your example, but with a value record (supposing the bitwise
> equivalence)
>
> value record C(double d) { }
> C c1 = new C(Double.longBitsToDouble(0x7ff0000000000001L));
> C c2 = new C(Double.longBitsToDouble(0x7ff0000000000002L));
>
> System.out.println(c1);  // C[d=NaN]
> System.out.println(c2);  // C[d=NaN]
> System.out.println(c1 == c2);  // false ??
> System.out.println(c1.equals(c2));  // true
>
>
> Sure. I mean, this is the exact same result as you get with an identity
> record, so I don't think it should be surprising. The fallacy, I think, is
> in expecting '==' for value objects to be something more than a
> substitutability test. (Which, like I said, could be done, but it seems
> like a distraction when what you really want to use is 'equals'.)
>
> Compare:
>
> value record S(String s) {}
> S s1 = new S("abc");
> S s2 = new S("abcd".substring(0,3));
>
> System.out.println(s1); // S[s=abc]
> System.out.println(s2); // S[s=abc]
> System.out.println(s1 == s2); // false
> System.out.println(s1.quals(s2)); // true
>
> This may be a new concept to learn: value objects with double fields can
> be 'equals' but not '==', just like value objects with reference fields can
> be 'equals' but not '=='. But I think that's a better quirk to learn than
> '==' sometimes not meaning "substitutable".
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/valhalla-spec-experts/attachments/20240209/6213b8d6/attachment-0001.htm>


More information about the valhalla-spec-experts mailing list