Part of the motivation is removing arbitrary limitations now that the translation machinery has caught up.  But more of it is, indeed, having to do with pattern matching.  And while you might think "who's going to match on floats", the reality is that this actually will happen more than you think, because of _nested patterns_.  (In general, if you see us talking about some sort of pattern matching corner case that you can't imagine using, it's because of nesting; this is where all the weirdness surrounding null came from.)

A pattern `Point(int x, int y)` looks like a single pattern, but really it's a nested pattern; this unrolls to:

    Point(\alpha, \beta) where \alpha matches int x && \beta matches int y

So while the Point components above look like a declaration, they are really just patterns.  And a pattern like `Point(var x, var y)` really means "look at the declared deconstructors of Point, do overload selection, and infer the types for `var` from that, and then treat it as a pattern with nested type-test patterns."

So, suppose we have a `Complex` type whose components are doubles. You might well want to match on something like:

    case Complex(var re, var im) where im == 0.0d  // keep it real, man

While this seems perfectly fine, the simplest and most natural way to define this means that we must be willing to match against the pattern "double im".  Once we've defined the semantics of matching against float types and constants, why would we restrict them from the constructs that support pattern matching?

Should we tell people "sorry, you can't match against classes like Complex, because floating point is hard"?

Sure, we could add all kinds of special cases to treat nested patterns in some non-uniform way to avoid having to define the semantics for floats, but that's more complicated and less expressive.

So the motivation is not so much "switching on float is the new hotness, everyone should do it", but "matching floats makes sense, and defining switching in terms of matching makes sense."

All that said, we could still special-case it by limiting the set of types that switch can apply to, so the pattern matching machinery on floats is there but switching is not.  But the burden should probably get flipped; it's not "why should we support it", but "why should we take steps to prevent it", since it falls out naturally via composition.


On 12/14/2017 11:09 AM, Kevin Bourrillion wrote:
Switch on long: sure.

Switch on float/double: why?

As someone who puts nontrivial effort into trying to get developers in my company to /stop/ ever depending on exact equality of floats and doubles, the only effect of this change will be to give me one additional thing to tell people not to do.

We already have ==, !=, equals, assertEquals, and using as a key in that list, so in a sense, "what's one more?". But -- were any actual advantages to doing this mentioned in this thread? I don't see them. It seems like the thread skipped right over that part?

If I had to guess, I'm guessing it might have something to do with the idea that Float/Double will automatically get supported by pattern-matching and there's nothing we can do about that. Is it something like that?





On Wed, Dec 13, 2017 at 5:44 PM, Paul Sandoz <[email protected] <mailto:[email protected]>> wrote:

    Recently i was mildly annoyed to discover that Float/DoubleBuffer
    provide another variant of equality different to that of
    Float.equals/Arrays.equals and ==, specifically:

    *   This method considers two float elements {@code a} and {@code b}
    *   to be equal if
    *   {@code (a == b) || (Float.isNaN(a) && Float.isNaN(b))}.
    *   The values {@code -0.0} and {@code +0.0} are considered to be
    *   equal, unlike {@link Float#equals(Object)}.

    The vectorized implementations for float/double
    comparison/equality leverage the equivalent of floatToRawIntBits
    and on a mismatch have to check if it was caused by NaNs and if
    continue the search. The equivalent vectorized implementation for
    buffers (in progress) needs to do the same for NaNs and +0/-0.

    I can imagine bit-wise comparison is problematic since IIUC the
    actually bit pattern of a NaN value can, in a platform specific
    manner, change depending on how it’s operated on.

    Paul.

    > On 13 Dec 2017, at 16:51, John Rose <[email protected]
    <mailto:[email protected]>> wrote:
    >
    > Joe's points make perfect sense to me.
    >
    > Because of distinct problems with float, double, and reference
    operand
    > types, the "==" operator of Java is a poor equivalence relation,
    so just
    > referring the semantics of switch to op== is IMO a false start
    for defining
    > switch.  Switch-on-string has already broken with that false
    start, in the
    > case of references, using Object.equals.  A coherent way to
    break from
    > op== on floats is to, also, refer to the closest possible
    Object.equals
    > method, that on Float (and Double).  Joe's proposal in fact
    appeals to
    > the same standards, that of floatToIntBits.
    >
    >
    
https://docs.oracle.com/javase/7/docs/api/java/lang/Float.html#equals(java.lang.Object)
    
<https://docs.oracle.com/javase/7/docs/api/java/lang/Float.html#equals%28java.lang.Object%29>
    >
    > The most fine-grained equality relation that can be defined
    across all
    > types does not have an API point, but it can be called
    "substitutability".
    > For references substitutability is simply acmp, or
    op==(Object,Object).
    > For floats, substitutability is approximated by equality on
    floatToIntBits,
    > but defined rigorously by equality on floatToRawIntBits, which
    preserves
    > distinctions among NaNs.  Since those distinctions can be
    observed by
    > code, two distinct NaNs cannot be said to be substitutable for each
    > other.
    >
    > Joe's comparison, and that of Float.equals, is slightly more
    coarse-grained
    > of an equivalence relation, because all the NaNs are grouped
    into a single
    > cluster.  I wish the designer of Float.equals had not stopped
    arbitrarily at
    > NaN folding, and used floatToRawIntBits.  But, given that
    history, I think
    > when switch supports floats and doubles, it will use Joe's
    comparison.
    >
    > As Remi points out, suitable third-party extractors (or value
    type wrappers)
    > can provide other relations besides Joe's default, either
    distinguishing
    > NaNs or lumping zeroes.  Perhaps even rejecting NaNs, since they
    aren't
    > equal to themselves, supposedly.
    >
    > But we only get to set the default once.  So perhaps we should delay
    > supporting floats directly, until we can put all three or four float
    > matching predicates in front of us and decide which is the default.
    >
    > I see no corresponding reason to delay longs. Instead, I see a
    pressing
    > need to figure out the correct relation between switch (x) {
    case (byte)1; }
    > where x might be a long or Long.  I don't see a way to delay
    that decision.
    >
    > Backing up a bit, I prefer to evaluate match semantics in terms
    of assignment
    > detection, rather than ad hoc equality predicates. If the story
    is only ad
    > hoc, "if this type then this predicate" I am sure it will have
    more nasty
    > corners than it needs.  If it has an overarching principle, then
    I am sure
    > it will have nasty corners (as with +0 and NaNs), but only a minimum
    > of them.  And the overarching principle I prefer for match is to
    ask the
    > following polymorphic question:  "Could a value just like this case
    > expression have been assigned to that switch variable?"  This, IMO,
    > unwinds a lot of otherwise ad hoc special pleading.  It does require
    > some ad hoc definition of what "just like this" means, but the
    rest falls
    > out of prior JLS semantics.  Including the vexed questions which
    will
    > be occurring to you, above, about Long vs. long vs. byte.
    >
    > — John
    >
    > On Dec 12, 2017, at 1:52 PM, Remi Forax <[email protected]
    <mailto:[email protected]>> wrote:
    >>
    >> While we could do that, use bits representation for float and
    double, this is typically the kind of things that a user can also
    do with a record (a value type record ?) and a deconstructor, so
    in my opinion, we should not rush to implement this kind of switch
    given that we will soon provide a general mechanism to implement
    them outside of the JDK.
    >>
    >> Rémi
    >>
    >> De: "Brian Goetz" <[email protected]
    <mailto:[email protected]>>
    >> À: "amber-spec-experts" <[email protected]
    <mailto:[email protected]>>
    >> Envoyé: Lundi 11 Décembre 2017 22:25:34
    >> Objet: Switching on float/double/long
    >> A target of opportunity for the new switch JEP is to fill out
    the set of types that traditional switches can operate on --
    specifically float, double, and long.  The reason that we don't
    support these now is mostly an accident of history; the
    `tableswitch` and `lookupswitch` opcodes are int-based, so the
    compiler doesn't have a convenient target for translating these.
    As you've seen from the recent notes on switch translation, we're
    working towards using indy more broadly as a translation target
    for most switch constructs.  This makes it far easier to bust the
    limitations on switch argument types, and so this has been listed
    as a target of opportunity in the JEP (for both statement and
    expression switches.)
    >>
    >> Our resident floating-point expert, Joe Darcy, offers the
    following additional thoughts on the subject:
    >>
    >> -- Begin forwarded message
    >>
    >> Per a recent request from Brian, I've written a few thoughts
    about switching on floating-point values.
    >>
    >> To address some common misunderstandings of floating-point,
    while it is often recommended to *not* compare floating-point
    values for equality, it is perfectly well-defined to do such
    comparisons, it just might not do what you want
    >>
    >> For example, instead of
    >>
    >>     // Infinite loop since sum stored in d never exactly equals
    1.0, doh!
    >>     while(d != 1.0)\u000B
    >>         d += 0.1;
    >>
    >> use either
    >>
    >>     // Counted loop
    >>     for(int i = 0; i < 10; i++)\u000B
    >>         d += 0.1;
    >>
    >> or
    >>
    >>     // Stop when numerical threshold is met
    >>     while(d <= 1.0)\u000B
    >>         d += 0.1;
    >>
    >> depending on the semantics the loop is trying to capture.
    >>
    >> I've attached a slide from my JVMLS talk this year to help
    illustrate the semantic modeling going in in IEEE 754
    floating-point. Each of the 232 possible bit patterns of a float
    is some floating-point value, likewise for the 264 possible bit
    patterns of a double. However, from a Java language or JVM
    perspective, there are not 232 or 264 distinct values we need or
    want to distinguish in most cases. In particular, we almost always
    want to treat all bit patterns which encode a NaN as a single
    conceptual NaN. Another wrinkle concerns zero: IEEE 754 has both a
    positive zero and a negative zero. Why are there *two* zeros?
    Because there are two infinities.  The signed infinities and
    distinguished by divide (1.0/0.0 => +infinity, 1.0/-0.0 =>
    -infinity) and by various library functions.
    >>
    >> So we want to:
    >>
    >>     * Allow every distinct finite nonzero floating-point value
    to be  the case of a switch.
    >>     * Allow -0.0 and +0.0 to be treated separately.
    >>     * Allow -infinity and +infinity to be treated separately.
    >>     * Collapse all NaN representation as a single value.
    >>
    >> For the "Rounding" mapping in the diagram which goes from the
    extended real numbers to floating-point data, there is a nonempty
    segment of the real number line which maps to a given
    representable floating-point number. For example, besides the
    string "1.0" mapping exactly to the reprentable floating-point
    value 1.0, there is a region slightly small than 1
    (0.99999999999999999999...) which will round up to 1.0 and a
    region slightly larger than 1 (1.000000000000000001...) which will
    round down to 1 from decimal -> binary conversion. This would need
    to be factored into any distinctiveness requirements for the
    different arms of the switch. In other words
    >>
    >>     case 1.000000000000000001:
    >>     ....
    >>     case 0.99999999999999999999
    >>     ...
    >>
    >> would need to be rejected just as
    >>
    >>     case 0:
    >>     ....
    >>     case 00:
    >>
    >> is rejected.
    >>
    >> In terms of JDK 9 structures and operations, the following
    transformation of a float switch has what I think are reasonable
    semantics:
    >>
    >>     Replace each float case label y in the source with an int
    label resulting from floatToIntBits(y). Note that floatToIntBits
    is used for the mapping rather than floatToRawIntBits since we
    want NaNs to be grouped together.
    >>
    >>     Instead of switching on float value x, switch on
    floatToIntBits(x).
    >>
    >> HTH,
    >>
    >> -Joe
    >




--
Kevin Bourrillion | Java Librarian | Google, Inc. |[email protected] <mailto:[email protected]>

Reply via email to