wadhwasahil
wadhwasahil

Reputation: 468

Can't convert Double to Float inside a class of generic type

Trying to play around with generic type in scala I encountered myself with difficulty. Here is my code snippet.

 import scala.reflect.ClassTag

 class test[A:ClassTag] {
  private val arr: Array[A] = Array.tabulate(10){ x=>
    ((1.0 + x) / 5.0).asInstanceOf[A]
  }

  def apply(i: Int): A = arr(i)
}

val obj = new test[Float]
println(obj(1))

This code throws an error

java.lang.ClassCastException: java.lang.Double cannot be cast to java.lang.Float
    at scala.runtime.BoxesRunTime.unboxToFloat(scratch_1.scala:105)
    at scala.collection.mutable.ArrayBuilder$ofFloat.$plus$eq(scratch_1.scala:460)
    at scala.Array$.tabulate(scratch_1.scala:327)
    at #worksheet#.test.<init>(scratch_1.scala:4)
    at #worksheet#.obj$lzycompute(scratch_1.scala:11)
    at #worksheet#.obj(scratch_1.scala:11)
    at #worksheet#.#worksheet#(scratch_1.scala:11)

When A = Double type, then the code gives an output of type Double.

But when A = Float type, then it throws this error.

Any feedback here would be really useful.

Upvotes: 3

Views: 2504

Answers (3)

You can fix this by adding @specialized(Float) to the A: ClassTag type parameter.

Here's a slightly simplified version of your code with the added @specialized(Float) tag. (I also added @specialized(Int) to make the example more complete.)

import scala.reflect.ClassTag

class Test[@specialized(Float, Int) A: ClassTag] {
  def apply(count: Int): Array[A] = Array.tabulate(count) { 
    i => (1.5 + i).asInstanceOf[A];
  }
}

val floatTest = new Test[Float];
println(floatTest(5).mkString(", ")); // 1.5, 2.5, 3.5, 4.5, 5.5
// Works because code is specialized for float
// and primitive double is cast to primitive float.

val doubleTest = new Test[Double];
println(doubleTest(5).mkString(", ")); // 1.5, 2.5, 3.5, 4.5, 5.5
// Works although code is not specialized for double
// but Double object can be cast to Double object.

val intTest = new Test[Int];
println(intTest(5).mkString(", ")); // 1, 2, 3, 4, 5
// Works because code is specialized for int
// and primitive double is cast to primitive int.

val longTest = new Test[Long];
println(longTest(5).mkString(", ")); // ClassCastException
// Fails because code is not specialized for long
// and Double object cannot be cast to Long object.
// Works if you add @specialized(Long).

(The no-op cast from Double object to Double object is called identity conversion.)

You can also omit the list of types from @specialized. In this case, the class will be specialized for all primitive types:

class Test[@specialized A: ClassTag] {
...

Be aware though that this will generate a lot more class files. For example, if all Scala collection classes would be specialized for all primitive types, the Scala language library would suddenly become about ten times as large.


The root problem is this: The casting rules for boxed primitives are very different from the casting rules for unboxed primitives.

In this case (using Java syntax):

double d = 1.0 / 5.0; // 0.2 as a double primitive value
float f = (float)d; // 0.2f as a float primitive value

Works as expected. The cast from double to float is known as narrowing primitive conversion.

But this doesn't work:

java.lang.Double d = 1.0 / 5.0; // 0.2, but in a java.lang.Double object
java.lang.Float f = (java.lang.Float)d; // ClassCastException

No narrowing or widening conversion from java.lang.Double to java.lang.Float is possible because neither is a subclass of the other, and boxing and unboxing conversions are not applied either. More precisely, there is an implicit boxing conversion from double to java.lang.Double in the first line, but no implicit boxing or unboxing conversion is applied in the second line.

Scala makes the issue even more confusing because it tries to hide the distinction - Scala only has type Double, but no type double. Depending on context, the Scala type Double is sometimes (I think most of the time) mapped to the Java primitive type double and sometimes to the Java reference type java.lang.Double.

Upvotes: 2

Alexey Romanov
Alexey Romanov

Reputation: 170713

This is actually a few leaky abstractions interacting. .asInstanceOf[A] doesn't become .asInstanceOf[Float] when A happens to be Float; it's really .asInstanceOf[Object] which gives you in this case a java.lang.Double. It has to be that way because there is just one class test, not a separate version for different As, and .asInstanceOf[Object] is a completely different operation from .asInstanceOf[Float] in JVM bytecode.

The cast to Float is added later and invisible in your code, and then java.lang.Double can't be cast to it (unlike a Double!).

I think the most reasonable way is defining a typeclass (Numeric is mentioned in the comments, but it doesn't have the method we want).

trait FromDouble[A] {
  def apply(x: Double): A
}
object FromDouble {
  implicit object DoubleFromDouble extends FromDouble[Double] {
    def apply(x: Double) = x
  }
  implicit object FloatFromDouble extends FromDouble[Float] {
    def apply(x: Double) = x.toFloat
  }

  // optional, to simplify test.arr a bit
  def apply[A](x: Double)(implicit fromDouble: FromDouble[A]) = fromDouble(x)
}

class test[A : ClassTag : FromDouble] {
  private val arr: Array[A] = Array.tabulate(10){ x=>
    FromDouble[A]((1.0 + x) / 5.0)
  }

  def apply(i: Int): A = arr(i)
}

Upvotes: 1

Paul Janssens
Paul Janssens

Reputation: 722

use toFloat as in

https://alvinalexander.com/scala/how-to-convert-between-numeric-types-in-scala-int-long-float-double

(first google result for scala double to float)

Upvotes: -1

Related Questions