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.