The title describes a specific problem I encountered when trying to solve a more general problem: how to separate a type conversion concern from a calculation concern. If I can solve that larger problem by another means than partially applied functions, great!
I'm using a type class, NumberOps, to represent operations on numbers. This code is paired down, but still exhibits the problem and expresses my intent. The first part simply defines the type class and a couple of implementations.
trait NumberOps[T] { // the type class (simplified for debugging)
def neg(x: T): T // negate x
def abs(x: T): T // absolute value of x
// ... over 50 more operations
def toFloating(x:T):AnyVal // convert from native to Float or Double, preserving precision
def fromFloating(f:AnyVal):T // convert from Float or Double to native
// ... also to/from Integral and to/from Big
}
object NumberOps { // Implements NumberOps for each type
import language.implicitConversions
implicit object FloatOps extends NumberOps[Float] {
def neg(x: Float): Float = -x
def abs(x: Float): Float = x.abs
def toFloating(f:Float):Float = f
def fromFloating(x:AnyVal):Float = {
x match {
case f:Float => f
case d:Double => d.toFloat
}
}
}
implicit object DoubleOps extends NumberOps[Double] {
def neg(x: Double): Double = -x
def abs(x: Double): Double = x.abs
def toFloating(d:Double):Double = d
def fromFloating(x:AnyVal):Double = {
x match {
case f:Float => f.toDouble
case d:Double => d
}
}
}
// ... other implicits defined for all primitive types, plus BigInt, BigDec
} // NumberOps object
All well and good. But now I want to implement NumberOps for complex numbers. A complex number will be represented as a 2-element array of any numeric type already defined (i.e. all primitive types plus BigInt and BigDecimal).
The intent with this code is to avoid combinatorial explosion of number types with numeric operations. I had hoped to achieve this by separating Concern A (type conversion) from Concern B (generic calculation).
You'll notice that "Concern A" is embodied in def eval, while "Concern B" is defined as a generic method, f, and then passed as a partially applied function (f _) to method eval. This code depends on the earlier code.
object ImaginaryOps { // Implements NumberOps for complex numbers, as 2-element arrays of any numeric type
import language.implicitConversions
import reflect.ClassTag
import NumberOps._
implicit def ComplexOps[U: NumberOps : ClassTag]: NumberOps[Array[U]] = { // NumberOps[T] :: NumberOps[Array[U]]
val numOps = implicitly[NumberOps[U]]
type OpF2[V] = (V,V) => NumberOps[V] => (V,V) // equivalent to curried function: f[V](V,V)(NumberOps[V]):(V,V)
// Concern A: widen x,y from native type U to type V, evaluate function f, then convert the result back to native type U
def eval[V](x:U, y:U)(f:OpF2[V]):(U,U) = {
(numOps.toFloating(x), numOps.toFloating(y), f) match {
case (xf:Float, yf:Float, _:OpF2[Float] @unchecked) => // _:opF @unchecked permits compiler type inference on f
val (xv,yv) = f(xf.asInstanceOf[V], yf.asInstanceOf[V])(FloatOps.asInstanceOf[NumberOps[V]])
(numOps.fromFloating(xv.asInstanceOf[Float]), numOps.fromFloating(yv.asInstanceOf[Float]))
case (xd:Double, yd:Double, _:OpF2[Double] @unchecked) => // _:opF @unchecked permits compiler type inference on f
val (xv,yv) = f(xd.asInstanceOf[V], yd.asInstanceOf[V])(DoubleOps.asInstanceOf[NumberOps[V]])
(numOps.fromFloating(xv.asInstanceOf[Double]), numOps.fromFloating(yv.asInstanceOf[Double]))
}
} // eval
new NumberOps[Array[U]]{ // implement NumberOps for complex numbers of any type U
def neg(a: Array[U]): Array[U] = a match { case (Array(ax, ay)) =>
def f[V](xv:V, yv:V)(no:NumberOps[V]):(V,V) = (no.neg(xv), no.neg(yv)) // Concern B: the complex calculation
val (xu,yu) = eval(a(0), a(1))(f _) // combine Concern A (widening conversion) with Concern B (calculation)
a(0) = xu; a(1) = yu; a
}
def abs(a: Array[U]): Array[U] = a match { case (Array(ax, ay)) =>
def f[V](xv:V, yv:V)(no:NumberOps[V]):(V,V) = (no.abs(xv), no.abs(yv)) // Concern B: the complex calculation
val (xu,yu) = eval(a(0), a(1))(f _) // combine Concern A (widening conversion) with Concern B (calculation)
a(0) = xu; a(1) = yu; a
}
def toFloating(a:Array[U]):AnyVal = numOps.toFloating( a(0) )
def fromFloating(x:AnyVal):Array[U] = Array(numOps.fromFloating(x), numOps.fromFloating(x))
}
} // implicit def ComplexOps
} // ImaginaryOps object
object TestNumberOps {
def cxStr(a:Any) = { a match { case ad: Array[Double] => s"${ad(0)} + ${ad(1)}i" } }
def cxUnary[T:NumberOps](v: T)(unaryOp:T => T): T = {
val ops = implicitly[NumberOps[T]]
unaryOp(v)
}
def main(args:Array[String]) {
println("TestNo4")
import ImaginaryOps._
val complexDoubleOps = implicitly[NumberOps[Array[Double]]]
val complex1 = Array(1.0,1.0)
val neg1 = cxUnary(complex1)(complexDoubleOps.neg _)
val abs1 = cxUnary(neg1)(complexDoubleOps.abs _)
println(s"adz1 = ${cxStr(complex1)}, neg1 = ${cxStr(neg1)}, abs1 = ${cxStr(abs1)}, ")
}
} // TestNumberOps
Now this code compiles, but at runtime I get a class cast exception:
Exception in thread "main" java.lang.ClassCastException: java.lang.Double cannot be cast to scala.runtime.Nothing$
at ImaginaryOps$$anon$1$$anonfun$1.apply(Experiment4.scala:68)
at ImaginaryOps$.ImaginaryOps$$eval$1(Experiment4.scala:60)
at ImaginaryOps$$anon$1.neg(Experiment4.scala:68)
at TestNumberOps$$anonfun$3.apply(Experiment4.scala:97)
at TestNumberOps$$anonfun$3.apply(Experiment4.scala:97)
at TestNumberOps$.cxUnary(Experiment4.scala:89)
at TestNumberOps$.main(Experiment4.scala:97)
at TestNumberOps.main(Experiment4.scala)
I understand why this exception occurs. It's because the compiler couldn't resolve the type V of def f[V], so when it gets passed to method eval as (f _), its generic type V has been changed to scala.runtime.Nothing.
Having struggled without success and after searching futilely online, I'm hoping to find a useful suggestion here. Probably I'm making this harder than it is, but with Scala's strong type system there ought to be a solution. The problem is how to tell the compiler to use this type in evaluating this function.