Generic method to cast numeric union type to one of it's component type Scala 3

I have:

type AllNumericTypes = Double | Float | Long | Int | Short | Byte

And I want to get the value of AllNumericTypes and cast it to passed type for example Int

def castToSingleNumericType[NumericType <: AllNumericTypes](num: AllNumericTypes): NumericType = num match
            case d: Double => d.asInstanceOf[NumericType]
            case f: Float => f.asInstanceOf[NumericType]
            case l: Long => l.asInstanceOf[NumericType]
            case i: Int => i.asInstanceOf[NumericType]
            case s: Short => s.asInstanceOf[NumericType]
            case b: Byte => b.asInstanceOf[NumericType]

but when I try

scala> val t: AllNumericTypes = 2f
scala> castToSingleNumericType[Int](t)

I get:

java.lang.ClassCastException: class java.lang.Float cannot be cast to class java.lang.Integer (java.lang.Float and java.lang.Integer are in module java.base of loader 'bootstrap')
  at scala.runtime.BoxesRunTime.unboxToInt(BoxesRunTime.java:99)
  ... 36 elided

but this method work as expected:

def castToSingleNumericType(num: AllNumericTypes): Int = num match
    case d: Double => println("double"); d.asInstanceOf[Int]
    case f: Float => f.asInstanceOf[Int]
    case l: Long => l.asInstanceOf[Int]
    case i: Int => i.asInstanceOf[Int]
    case s: Short => s.asInstanceOf[Int]
    case b: Byte => b.asInstanceOf[Int]

scala> castToSingleNumericType(t)
val res1: Int = 2

didn’t find answer in older questions

It even gets better…

println(castToSingleNumericType[Int](t)) // 2.0
println(castToSingleNumericType[Int](t).getClass) // int

No idea what exactly is going on there…

The generic version triggers boxing, and conversion/“casting” doesn’t work with the boxed representations.

2.0f.asInstanceOf[Int] // works
java.lang.Float.valueOf(2.0f).asInstanceOf[java.lang.Integer] // ClassCastException

In Scala 2, this might be fixed via specialization, but it looks like there is no equivalent mechanism in Scala 3 (yet?). However, there’s inlining which covers some of the use cases, which seems to include this one:

inline def castToSingleNumericType[NumericType <: AllNumericTypes](num: AllNumericTypes): NumericType = ???

(Not sure if and where this may break for specific application scenarios, though.)

That being said, even knowing that it’s specified that “casting” numeric values via #asInstanceOf is translated to a numeric conversion, it still looks somewhat wrong to me.

You need to define a typeclass that can convert one numeric type to another. asInstanceOf really should only be used for casting, not conversion, and in any case you might accidentally box your primitive types (like a generic function), you’re going to get a casting exception.

Unpacking that a little more (because this is a common FAQ, and easy to misunderstand): asInstanceOf doesn’t do anything. It is essentially a compiler directive that means “this value of type A is already a value of type B, and you should just believe me”. It’s how you work around the compiler not having enough information, but it doesn’t change A to B.

So the runtime ClassCastException is what I would normally expect to happen: you’re lying to the compiler, telling it that a Float is actually an Int – I would expect that to crash at runtime. To me, the surprise is that it’s sometimes not doing so – it must be hitting some sort of automatic conversion by accident, probably entirely separately from the asInstanceOf.

But just generally, asInstanceOf isn’t the right tool to use for this sort of problem – you’re trying to convert a value from one runtime class to another, and asInstanceOf doesn’t do that.

2 Likes

For primitive numeric values, it does.

Welcome to Scala 2.13.5 (OpenJDK 64-Bit Server VM, Java 15.0.1).
Type in expressions for evaluation. Or try :help.

scala> 3.14f.asInstanceOf[Int]
val res0: Int = 3

scala> 3.asInstanceOf[Float]
val res1: Float = 3.0

It seems that scala-lang.org isn’t accessible right now (at least not for me), so I can’t check the spec, but a web search shows me this preview snippet, for example:

asInstanceOf[$T$] is treated specially if $T$ is a numeric value type. In this case the cast will be translated to an application of a conversion method …
https://www.scala-lang.org/files/archive/spec/2.11/14-the-scala-standard-library.html

As noted above, this usage looks fishy to me, as well, but it does work.

yes, but when you write a generic function, you’re no longer dealing with primitive numeric values.

I think it’s confusing because scala uses the same type for two concepts. Int the primitive is a simple 4 byte value on the jvm. Int the object is an actual object. In java, these are seperate types, int and Integer. In the failed code, you’ll notice it complains that you cannot cast Float to Integer. That means in that case it is trying to cast one object to another. Conversion is not done in that case, and if Float cannot be viewed as an Integer (and it cannot be), then the cast fails. Meanwhile, you can cast from float to int because automatic conversion is done for these (a holdover from c/c++).

Java generics are all based on class instances that are children of the Object type. int and float are not descendants of Object, nor are they descendants of any other type. In order to be able to use primitive types like these with generic functions and classes, java boxes them to class types that are children of Object, Float and Integer.

Casting on the jvm only does conversion in the case of a primitive type, and so it cannot be relied on ever in the context of a generic function. In order to get the results you want, you need to define a type class for the “primitive types” in scala that specifies how to convert from one to the other.

1 Like

Allow me to add to the confusion. f.asInstanceOf[NumericType] doesn’t actually do anything at runtime other than returning (a boxed version of) f. Because NumericType is an unbounded type variable it would be like (f: java.lang.Float).asInstanceOf[Object]. The compiler actually adds a cast after the call to castToSingleNumericType[Int](t). Because generics are erased, castToSingleNumericType actually returns Object. But the type signature in Scala promises that it returns java.lang.Integer (because non-specialized generics are always boxed). So the compiler simply adds bytecode that casts the result to java.lang.Integer.

IMHO f.asInstanceOf[Int] should not convert between primitives like Java does. In Scala we have .toInt and friends for that. asInstanceOf between primitives should do exactly the same as it does for other types—like @jducoeur explained. Otherwise it only creates confusion, as evidenced by this thread.

2 Likes

Assuming you’re using Scala 3.x, you can use the transparent feature:

transparent inline def x: NumericType = 5

will be typechecked as Int.

See Inline | Macros in Scala 3 | Scala Documentation

1 Like