Can someone help me understand what’s happening here? Perhaps the deprecated warning message is simply misleading?
for { radians <- -Pi to Pi by Pi/20}
println(s"radian=$radians cos=${cos(radians)}")
IntelliJ tells me to is deprecated, and that I shoudl use BigDecimal range.
However, if I try to use BigDecimal range, I get other problems.
Error:(80, 43) type mismatch;
found : scala.math.BigDecimal
required: Double
println(s"radian=$radians cos=${cos(radians)}")
Here is the code.
def main(argv:Array[String]):Unit = {
for { radians <- -Pi to Pi by Pi/20}
println(s"radian=$radians cos=${cos(radians)}")
for { radians <- BigDecimal(-Pi) to Pi by Pi/20}
println(s"radian=$radians cos=${cos(radians)}")
}
val Pi = BigDecimal(math.Pi)
for ( radians <- -Pi to Pi by Pi / 21 )
println(s"radian=$radians cos=${math.cos(radians.toDouble)}")
java.lang.IllegalArgumentException: Precision 34 inadequate to represent steps of size 0.1495996501709425238095238095238095 near -3.141592653589793
at scala.collection.immutable.NumericRange$.FAIL$1(NumericRange.scala:271)
at scala.collection.immutable.NumericRange$.bigDecimalCheckUnderflow(NumericRange.scala:274)
at scala.collection.immutable.NumericRange$.count(NumericRange.scala:315)
at scala.collection.immutable.NumericRange.length$lzycompute(NumericRange.scala:75)
at scala.collection.immutable.NumericRange.length(NumericRange.scala:75)
at scala.collection.immutable.NumericRange.foreach$mVc$sp(NumericRange.scala:110)
at Playground$.<clinit>(main.scala:4)
... 13 more
There was a fairly extensive discussion here about this issue when I asked about it a while back. The underlying issue IIRC is that some decimal numbers are not accurately represented in binary (e.g., 0.2). So the ends of the speciied range can be misleading.
I wasn’t suggesting that iterating across BigDecimal is acceptable. Quite the contrary - I’ve provided an example where trying to iterate across BigDecimals results in an exception at runtime.
If the proper solution is to use integer iteration, then it seems the deprecation warning is wrong. It suggests using BigDecimal iteration, which leads to exceptions like you showed above. Or did I misunderstand the deprecation warning?
Once I understand the issue, it’s not really very difficult to make my own application specific range function which does what I want, or 100s of variants of suchy.
def doubleRange(from:Double, to:Double, step:Double) = {
val n = ((to - from) / step).floor.toInt
for { i <- (0 until n).view
x = from + i*step } yield x
}
(for {x <- doubleRange(-Pi, Pi, Pi/21)
c = cos(x)} println(s"x=$x cos(x)=$c"))
or maybe the following, depending on the desired semantics
def doubleRange(lower:Double, upper:Double, steps:Int) = {
val step = (upper - lower)/steps
for { i <- (0 to steps).view
x = lower+ i*step
} yield x.min(upper)
}
(for {x <- doubleRange(-Pi, Pi, 21)
c = cos(x)} println(s"x=$x cos(x)=$c"))
Now there could be a discrepancy between what Scala stdlib authors consider desirable and what I consider desirable. If I had the task of implementing ranges in Scala today I would implement only ranges over integral numbers. Iterating over floating-point numbers should be left for specialized libraries.
That looks OK. Iteration is done over integers so limited accuracy of floating-point data types doesn’t affect the number of iterations.
In my application, iterating by increasing number-of-steps. I’m computing a value by averaging over several steps, and the repeating with increased number of steps, until convergence is reached.
Try it out on (6.1, 7.3, 0.3). Then try it out on (8.3, 12.2, 0.3).
The reason is that 7.3 - 6.1 = 1.2000000000000002 (a little bit too much), while 8.3 - 12.2 = 3.8999999999999986 (not quite enough).
So you can’t do the exact math you need to do in order for the range to work.
If you use a BigDecimal, you at least have the ability to set up the numbers to work out right.
You can verify the following
(BigDecimal(6.1) to BigDecimal(7.3) by BigDecimal(0.3)).size == 5
(BigDecimal(8.3) to BigDecimal(12.2) by BigDecimal(0.3)).size == 14
which is at least consistent (includes endpoints).
This doesn’t help you calculate a step size precisely, if you want a particular number of steps. But at least you get the right behavior if you deliver the right endpoints and step size, somehow.
The Double behavior is just broken; the first one has 5 elements but the second 13. And the behavior of what you wrote is broken in the same way for the same reason.
First of all I think there was a blatent error in the function aside from the round-off error. We need to replace until with to, otherwise it only divides the interval up into n-1 pieces.
def doubleRange(from:Double, to:Double, step:Double) = {
val n = ((to - from) / step).floor.toInt
for { i <- (0 to n).view
x = from + i*step } yield x
}
But aside from that to/until error, you are right that the right-most interval is not handled correct.
The question of what to do with a final (rightmost) interval which is smaller than the step. If it is just epsilon smaller than step, you probably want to keep it, but if the interval is of size epsilon you probably want to omit it.
Here is a version of the function which always retains the final step, even if its size is miniscule.
def doubleRange(from:Double, to:Double, step:Double) = {
require(step>0 && from <= to)
val n = ((to - from) / step).ceil.toInt
for { i <- (0 to n).view
x = from + i*step
if i == 0 || x.min(to) != (from+(i-1)*step).min(to)
} yield x.min(to)
}
Actually I think the steps:Int version has a bug. If lower > upper it will step downward. This means the yield x.min(upper) should be changed to x.max(upper) whenever lower > upper. Right?
def doubleRange(lower:Double, upper:Double, steps:Int) = {
require(steps > 0 && lower != upper)
val step = (upper - lower)/steps
for { i <- (0 to steps).view
x = lower+ i*step
} yield if (lower < upper) x.min(upper) else x.max(upper)
}
… and potentially different code path in the case that lower==upper. I’m not sure what it should do in that case.