-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Invariant[Fractional] and MiniFloat #3813
Conversation
…he methods on java.lang.Math since these are not available in ScalaJS
…o AllInstances trait for 2.12 where it was previously missing
@@ -67,3 +69,7 @@ trait AllInstancesBinCompat5 extends SortedSetInstancesBinCompat0 | |||
trait AllInstancesBinCompat6 extends SortedSetInstancesBinCompat1 with SortedMapInstancesBinCompat2 | |||
|
|||
trait AllInstancesBinCompat7 extends SeqInstances | |||
|
|||
trait AllInstancesBinCompat8 extends InvariantInstances |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that I missed adding the InvariantInstances
to the 2.12 traits in #3773, so the cats.instances.all._
import didn't include them in 2.12. This is fixed here.
* to its small domain. It is only approximately commutative and associative under addition and multiplication, due to | ||
* floating-point errors, overflows, and the behaviour of `NaN`. | ||
* | ||
* Note that unlike `Float`, `MiniFloat` does not support the representation of negative zero (`-0f`). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that I've made the decision not to reproduce Float
's behaviour around -0f
. This lets us ignore the complications around having two distinct values for 0
that are considered equal by any Eq
definitions we are interested in using.
override def equals(other: Any): Boolean = other match { | ||
case that: MiniFloat => this.toFloat == that.toFloat | ||
case _ => false | ||
} | ||
|
||
override def hashCode: Int = java.lang.Float.hashCode(toFloat) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This replicates the universal equality behaviour of Float
, which is not consistent with the decision I've made below to define Eq[MiniFloat]
as considering NaN
as equal to itself. I will discuss this below.
private[MiniFloat] val base = 2 | ||
|
||
private val minSignificand = -2 | ||
private val maxSignificand = 2 | ||
|
||
private val minExponent = -1 | ||
private val maxExponent = 2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These define the range of MiniFloat
. The finite values currently are:
-8.0
-4.0
-2.0
-1.0
-0.5
0.0
0.5
1.0
2.0
4.0
8.0
private val floatExponentStartBit: Int = 23 | ||
private val floatExponentLength: Int = 8 | ||
private val floatExponentBias: Int = 127 | ||
private val floatExponentMask: Int = ((1 << floatExponentLength) - 1) << floatExponentStartBit | ||
|
||
// This does the same thing as java.lang.Math.getExponent, but that method is not available in scalaJS so we have to | ||
// do the same thing here. | ||
private def getExponent(float: Float): Int = | ||
((floatExponentMask & java.lang.Float.floatToIntBits(float)) >> floatExponentStartBit) - floatExponentBias |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that I've had to write a dedicated function to extract the exponent from a Float
since java.lang.Math.getExponent
is not currently defined in ScalaJS. I'll probably look to add this over there soon.
} | ||
|
||
protected def versionSpecificNumericEq[A: Eq: ExhaustiveCheck]: Eq[Numeric[A]] = Eq.allEqual |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are no version-specific components of Numeric
in 2.12, as opposed to 2.13 which added parseString
.
implicit protected def eqNumeric[A: Eq: ExhaustiveCheck]: Eq[Numeric[A]] = Eq.by { numeric => | ||
// This allows us to catch the case where the fromInt overflows. We use the None to compare two Numeric instances, | ||
// verifying that when fromInt throws for one, it throws for the other. | ||
val fromMiniInt: MiniInt => Option[A] = | ||
miniInt => | ||
try Some(numeric.fromInt(miniInt.toInt)) | ||
catch { | ||
case _: IllegalArgumentException => None // MiniInt overflow | ||
} | ||
|
||
( | ||
numeric.compare _, | ||
numeric.plus _, | ||
numeric.minus _, | ||
numeric.times _, | ||
numeric.negate _, | ||
fromMiniInt, | ||
numeric.toInt _, | ||
numeric.toLong _, | ||
numeric.toFloat _, | ||
numeric.toDouble _ | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of this code for defining an Eq[Numeric]
is common between 2.12 and 2.13, so I've refactored to move the common stuff into the common file. Only the version-specific behaviour sits in ScalaVersionSpecific.scala
via the versionSpecificNumericEq
below.
) | ||
} | ||
|
||
Eq.and(versionSpecificNumericEq, versionAgnosticNumericEq) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Open to feedback about this approach of breaking the version-specific equality checks out into a dedicated Eq
in ScalaVersionSpecific.scala
.
import org.scalacheck.Prop._ | ||
import org.scalacheck.{Arbitrary, Gen} | ||
|
||
class MiniFloatSuite extends CatsSuite { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a lot of tests in here, which I hope is OK. I wanted to be thorough when checking the corner-case floating point behaviours.
} | ||
} | ||
|
||
testSpecialNumberExpectations(MiniFloat.NaN, expectIsNaN = true, expectIsFinite = false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test fails in Scala Native due to scala-native/scala-native#2178.
I'd personally like to separate the PR. One for the I'm more skeptical to MiniFloat. I think something like Float16: or BFloat16: would be more useful. It is small enough to enumerate still, but also potentially useful for low precision applications. |
I probably could have been clearer in the issue description, but these are bundled together in this PR because In order to define an
No type currently exists for which you can define both If the scope of this PR is too big I'm happy to use the deprecated
I might be missing something, but a Float16 type has a cardinality of roughly 2¹⁶ = 65,536 (a bit less if you equate all NaNs). This seems much too big for Also, my understanding was that |
I'll break this into two PRs. |
Fixes #3794
This PR adds
Invariant[Fractional]
. In order to test this instance, I have provided a new classMiniFloat
, which is the floating-point analogue toMiniInt
. Its range is small enough that anExhaustiveCheck
instance can be defined.I'm conscious that a class like
MiniFloat
is a more significant addition than might be strictly necessary to add an instance likeInvariant[Fractional]
, but I'm hoping that it's judged to be sufficiently useful to be included in thediscipline
library.I'll make more explanatory comments inline on the PR.
Note that the build is failing due to a bug in the Scala Native implementation of isFinite. I'll try to find a solution to ignore this test.