Inlining can give you some âabstraction without regretâ with regard to type classes such as in that example, but itâs a totally different thing than what @specialized
does.
trait Numeric[A] {
def zero: A
extension (a: A) {
def +(b: A): A
def *(b: A): A
def -(b: A): A
def /(b: A): A
}
}
given Numeric[Double] {
inline def zero = 0.0
extension (a: Double) {
inline def +(b: Double) = a + b
inline def *(b: Double) = a * b
inline def -(b: Double) = a - b
inline def /(b: Double) = a / b
}
}
abstract class MathLib[N: Numeric] {
def computeThing(a: N, b: N): N
}
object MathLib {
inline def apply[N: Numeric] = new MathLib[N] {
def computeThing(a: N, b: N): N = {
(a + b) * (a - b)
}
}
}
Now we have written one generic method that can work on all Numeric
types, and generate efficient code without having to incur all the usual indirection.
What this doesnât do however is eliminate boxing, like @specialized
does. When you call MathLib[Double].computeThing(4.2, 2.4)
the compiler will generate bytecode equivalent to the following:
final class $anon() extends MathLib[Double](given_Numeric_Double) {
def computeThing(a: Double, b: Double): Double = {
(a + b) * (a - b)
}
override def computeThing(a: Object, b: Object): Object = {
Double.box(computeThing(Double.unbox(a), Double.unbox(b)))
}
}
val a: MathLib[Double] = new $anon()
Double.unbox(a.computeThing(Double.box(4.2), Double.box(2.4)))
So boxing and unboxing still happens (twice). The bytecode optimizerâif enabledâmay still be able to optimize this away though, if Scala 3 can reuse the 2.13 optimizer.
@specialized
does a lot of fragile magic so the compiler knows at the call sites that heâs dealing with a specialized class and call the specialized methods directly.