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.

1 Answers

0
yw3410 On

What you want to do is to use a derived type class for your complex number.

Consider the following simplified scenario,

 trait Addable[A] {
   def apply(a: A, b: A): A
 }

 implicit val intAddable: Addable[Int] = new Addable[Int] {
    def apply(a: Int, b: Int): Float = a + b
 }

 implicit val floatAddable: Addable[Float] = new Addable[Float] {
   def apply(a: Float, b: Float): Float = a + b
 }

 implicit final class AddOps[A](a: A) {
   def add(b: A)(implicit addable: Addable[A]): A = addable(a, b)
 }

which basically allows us to call, 1.add(2) allowing the scala compiler to infer that there is an addable for ints.

However what about for your complex type? Since we want to essentially say there exists an addable for any complex type which is composited of 2 types which follow the addable law we essentially define it like this,

 implicit def complexAddable[A](implicit addable: Addable[A]): Addable[Array[A]] = {
     new Addable[Array[A]] {
         def apply(a: Array[A], b: Array[A]): Array[A] = {
            Array(a(0).add(b(0)), a(1).add(b(1)))
         }
     }
}

which works because there is an Addable[A] in scope. Note that this of course the implicit cannot be created if an addable for A doesn't exist, and hence you have lovely compile time safety.

You can find usages of this pattern in the excellent functional libraries such as scalaz, cats, scodec et cetera, and is known from haskell as the type class pattern.