Reputation: 468
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
Reputation: 3587
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
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 A
s, 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
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