Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Weak conformance to Float and Double are incorrect #10773

Closed
eed3si9n opened this issue Mar 13, 2018 · 14 comments · Fixed by scala/scala#8757
Closed

Weak conformance to Float and Double are incorrect #10773

eed3si9n opened this issue Mar 13, 2018 · 14 comments · Fixed by scala/scala#8757
Labels
Milestone

Comments

@eed3si9n
Copy link
Member

eed3si9n commented Mar 13, 2018

Related: https://contributors.scala-lang.org/t/can-we-get-rid-of-cooperative-equality/1131/85
Related: scala/scala#6398

steps

scala> Set[Float](2147483584, 2147483645, 2147483646, 2147483647)

scala> Set[Double](99999999991234561L, 99999999991234562L, 99999999991234563L)

problem

scala> Set[Float](2147483584, 2147483645, 2147483646, 2147483647)
res1: scala.collection.immutable.Set[Float] = Set(2.14748365E9)

scala> Set[Double](99999999991234561L, 99999999991234562L, 99999999991234563L)
res2: scala.collection.immutable.Set[Double] = Set(9.999999999123456E16)

expectation

It should not compile. Similar to using literal in Set[Byte].

scala> Set[Byte](1, 2)
res0: scala.collection.immutable.Set[Byte] = Set(1, 2)

scala> Set[Byte](1, 2, 128)
                       ^
       error: type mismatch;
        found   : Int(128)
        required: Byte

note

Here's what SLS says: https://github.com/scala/scala/blob/2.13.x/spec/03-types.md

Weak Conformance

In some situations Scala uses a more general conformance relation.
A type $S$ weakly conforms to a type $T$, written $S <:_w T$,
if $S <: T$ or both $S$ and $T$ are primitive number types and $S$ precedes $T$ in the following ordering.

Byte  $<:_w$ Short
Short $<:_w$ Int
Char  $<:_w$ Int
Int   $<:_w$ Long
Long  $<:_w$ Float
Float $<:_w$ Double

IEEE floats

https://en.wikipedia.org/wiki/IEEE_754 has nice writeup of IEEE floats. The useful part is the notion of significand. Float has 24 bits of it, and Double 53 bits.

Given that Int has 32 bits, Float is unable to represent all integers Int can represent.
Same goes for Long vs Double.

Option 1: Split the floats

My suggestion would be to remove "Long <:_w Float" from the spec.

The breaking change about this is that small integers like 0 and 1 no longer will automatically "widen" to Float and Double, but I am personally ok with it.

@NthPortal
Copy link

Would it still be safe for Int to automatically widen to Double? Since they are the default types for integer and floating point numbers respectively, it should significantly reduce the pain of numbers like 0 and 1 needing manual conversion.

@eed3si9n
Copy link
Member Author

eed3si9n commented Mar 13, 2018

I think so. That's why I wrote "Option 1".

Option 2: Replace Long <:_w Float with Int <:_w Double

Pro as noted above by @NthPortal:

Since they are the default types for integer and floating point numbers respectively, it should significantly reduce the pain of numbers like 0 and 1 needing manual conversion.

@sjrd
Copy link
Member

sjrd commented Mar 13, 2018

Yes, widening Int to Double is perfectly safe.

@Ichoran
Copy link

Ichoran commented Jun 11, 2018

Can we please widen literals that fit losslessly? It's a pain to have to write 0f every time you just mean zero. The compiler is in a perfect position to tell. After all, it already does for integers that are too large. (See behavior with Short vs Int, for example.)

@lrytz
Copy link
Member

lrytz commented Feb 3, 2020

This didn't get entirely fixed by scala/scala#8679 / scala/scala#7405, there is no warning for literals yet.

Welcome to Scala 2.13.2-20200203-122414-bc05029 (Java HotSpot(TM) 64-Bit GraalVM EE 19.2.1, Java 1.8.0_231).
Type in expressions for evaluation. Or try :help.

scala> Set[Float](2147483584, 2147483645, 2147483646, 2147483647)
val res0: scala.collection.immutable.Set[Float] = Set(2.14748365E9)

scala> val (a, b, c, d) = (2147483584, 2147483645, 2147483646, 2147483647)
val a: Int = 2147483584
val b: Int = 2147483645
val c: Int = 2147483646
val d: Int = 2147483647

scala> Set[Float](a, b, c, d)
                  ^
       warning: Automatic conversion from Int to Float is deprecated (since 2.13.1) because it loses precision. Write `.toFloat` instead.
                     ^
       warning: Automatic conversion from Int to Float is deprecated (since 2.13.1) because it loses precision. Write `.toFloat` instead.
                        ^
       warning: Automatic conversion from Int to Float is deprecated (since 2.13.1) because it loses precision. Write `.toFloat` instead.
                           ^
       warning: Automatic conversion from Int to Float is deprecated (since 2.13.1) because it loses precision. Write `.toFloat` instead.
val res1: scala.collection.immutable.Set[Float] = Set(2.14748365E9)

cc @smarter, @som-snytt.

@lrytz
Copy link
Member

lrytz commented Feb 3, 2020

Also this should warn:

scala> 16777217f
val res0: Float = 1.6777216E7

@lrytz lrytz modified the milestones: Backlog, 2.13.2 Feb 3, 2020
@som-snytt
Copy link

@lrytz thanks, I noticed that from the t8450 test. I started looking at constants then got distracted by dotty harmonization.

@sjrd
Copy link
Member

sjrd commented Feb 3, 2020

Also this should warn:

scala> 16777217f
val res0: Float = 1.6777216E7

If you warn for that, then you should also warn for 1.6777217f and any other floating point that is represented differently. How are you going to do that?

@lrytz
Copy link
Member

lrytz commented Feb 3, 2020

Right, float literals is a different issue, and not urgent. scala> 16777217: Float should warn.

@som-snytt
Copy link

In Scala 3, you have to notice what was inferred. Hopefully, you'll have -Xlint -Werror.

scala> List(16777217, 3.14f)                                                                                                  
val res4: List[AnyVal] = List(16777217, 3.14)

scala> List(16777216, 3.14f)                                                                                                  
val res5: List[Float] = List(1.6777216E7, 3.14)

The test is round-tripping.

To warn on float, you can check the double.

scala> 16777217d == 16777217f
res1: Boolean = false

I remember the parse issue scala/scala#5648

@SethTisue SethTisue modified the milestones: 2.13.2, 2.13.3 Feb 6, 2020
@som-snytt
Copy link

Follow-up will be at scala/scala#8730

My question is: How is 1f + lossy morally different from def f(x: Float) = ??? ; f(lossy)?

It seems to me, it should warn on addition as though it were defined def +(x: Float).

@smarter
Copy link
Member

smarter commented Feb 20, 2020

How is 1f + lossy morally different from def f(x: Float) = ??? ; f(lossy)?

I'd say the main difference is that you don't even need an int to float conversion to be lossy for +: adding two floats can also lose precision (or overflow/underflow), therefore one always needs to be careful about these operations, so I don't think that extra warnings when ints are involved would help here.

@eed3si9n
Copy link
Member Author

SLS 6.26.1 talks about Numeric Literal Narrowing

If the expected type is Byte, Short or Char, and the expression 𝑒 is an integer literal fitting in the range of that type, it is converted to the same literal in that type.

scala> Set[Byte](1, 2)
res0: scala.collection.immutable.Set[Byte] = Set(1, 2)

scala> Set[Byte](1, 2, 128)
                       ^
       error: type mismatch;
        found   : Int(128)
        required: Byte

Given that Float's mantissa is narrower than Int, we can probably update the SLS to say integer literal above 16777215 (0xFFFFFF) would be at the mercy of rounding, and thus, out of range.

@som-snytt
Copy link

I stopped worrying about newbie traps like x += 1 doing nothing; we'll leave it for a linter.

I did update the spec. I used the test for harmonization, but with more awkward words.

A conceptual trap is that "precision" doesn't mean "significant digits".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
9 participants