Symbol to is deprecated. use BigDecimal range instead

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.

Screenshot 2020-05-17 at 10.39.21

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)}")
  }

I can work around this by making an integer range, then computing radians separately.
Is this what the programmer is intended to do?

for {n <- 0 to 20
     radians = -Pi + n * 2 * Pi / 20
     s1 = sin(radians)
     } println(s"radians =$radians sin=$s1")

Use bigDecimal.toDouble:

val Pi = BigDecimal(math.Pi)
for ( radians <- -Pi to Pi by Pi / 20 )
  println(s"radian=$radians cos=${math.cos(radians.toDouble)}")

However I wouldn’t recommend relying on exact division. Below code throws exception https://scastie.scala-lang.org/kDZlnGBGRR2BxFG7tPwjeQ

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

Floating-point ranges lead to floating-point exact comparisons which are suspectible to https://en.wikipedia.org/wiki/Floating-point_arithmetic#Accuracy_problems . Working on integers as much as possible and only then converting to floating-point is more likely to achieve https://en.wikipedia.org/wiki/Numerical_stability When you need 20 steps in a for-loop then do something like for (i <- 0 until 20) ... (like you did in last approach).

2 Likes

So I don’t understand the logic, why is it acceptable to iterate across a BigDecimal range bit not a Double range.

A BigDecimal es precise, a Double is not.

I don’t believe the claim that BigDecimal(Pi) is precise but Pi is not.

No, any BigDecimal made out of a Double is already unprecise.
But, that is not the point, the point is that the range can be precise.

2 Likes

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.

1 Like

Being precise here does not mean it represents the real value precisely, but that we can do addition and subtraction without loosing precision.

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.

1 Like

It sounds indeed like the deprecation warning is misleading.

It was done on purpose: https://github.com/scala/scala/commit/340b899536f767ccb6fc49d13879cdcacab3999d

So to recap:

Yes. Also: it’s not a workaround, it’s the proper solution.

1 Like

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.

That’s fine if you know the number of steps in advance, but the more typical scenario in my experience is to want a given step size.

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.

Um, this doesn’t do what you want. It isn’t easy.

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.

1 Like

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. :frowning:

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.