views:

188

answers:

2

I'd like to implement a class C to store values of various numeric types, as well as boolean. Furthermore, I'd like to be able to operate on instances of this class, between types, converting where necessary Int --> Double and Boolean -> Int, i.e., to be able to add Boolean + Boolean, Int + Boolean, Boolean + Int, Int + Double, Double + Double etc., returning the smallest possible type (Int or Double) whenever possible.

So far I came up with this:

abstract class SemiGroup[A] { def add(x:A, y:A):A }

class C[A] (val n:A) (implicit val s:SemiGroup[A]) {
  def +[T <% A](that:C[T]) = s.add(this.n, that.n)
}

object Test extends Application {
  implicit object IntSemiGroup extends SemiGroup[Int] { 
    def add(x: Int, y: Int):Int = x + y 
  }

  implicit object DoubleSemiGroup extends SemiGroup[Double] { 
    def add(x: Double, y: Double):Double = x + y 
  }

  implicit object BooleanSemiGroup extends SemiGroup[Boolean] { 
    def add(x: Boolean, y: Boolean):Boolean = true;
  }

  implicit def bool2int(b:Boolean):Int = if(b) 1 else 0

  val n = new C[Int](10)
  val d = new C[Double](10.5)
  val b = new C[Boolean](true)

  println(d + n)    // [1]
  println(n + n)    // [2]
  println(n + b)    // [3]
  // println(n + d)    [4] XXX - no implicit conversion of Double to Int exists
  // println(b + n)    [5] XXX - no implicit conversion of Int to Boolean exists
}

This works for some cases (1, 2, 3) but doesn't for (4, 5). The reason is that there is implicit widening of type from lower to higher, but not the other way. In a way, the method

def +[T <% A](that:C[T]) = s.add(this.n, that.n)

somehow needs to have a partner method that would look something like:

def +[T, A <% T](that:C[T]):T = that.s.add(this.n, that.n)

but that does not compile for two reasons, firstly that the compiler cannot convert this.n to type T (even though we specify view bound A <% T), and, secondly, that even if it were able to convert this.n, after type erasure the two + methods become ambiguous.

Sorry this is so long. Any help would be much appreciated! Otherwise it seems I have to write out all the operations between all the types explicitly. And it would get hairy if I had to add extra types (Complex is next on the menu...).

Maybe someone has another way to achieve all this altogether? Feels like there's something simple I'm overlooking.

Thanks in advance!

+3  A: 

There's a way to do that, but I'll leave it to retronym to explain it, since he wrote this solution. :-)

Daniel
+5  A: 

Okay then, Daniel!

I've restricted the solution to ignore Boolean, and only work with AnyVals that have a weak Least Upper Bound that has an instance of Numeric. These restrictions are arbitrary, you could remove them and encode your own weak conformance relationship between types -- the implementation of a2b and a2c could perform some conversion.

Its interesting to consider how implicit parameters can simulate inheritance (passing implicit parameters of type (Derived => Base) or Weak Conformance. They are really powerful, especially when the type inferencer helps you out.

First, we need a type class to represent the Weak Least Upper Bound of all pairs of types A and B that we are interested in.

sealed trait WeakConformance[A <: AnyVal, B <: AnyVal, C] {
  implicit def aToC(a: A): C

  implicit def bToC(b: B): C
}

object WeakConformance {
  implicit def SameSame[T <: AnyVal]: WeakConformance[T, T, T] = new WeakConformance[T, T, T] {
    implicit def aToC(a: T): T = a

    implicit def bToC(b: T): T = b
  }

  implicit def IntDouble: WeakConformance[Int, Double, Double] = new WeakConformance[Int, Double, Double] {
    implicit def aToC(a: Int) = a

    implicit def bToC(b: Double) = b
  }

  implicit def DoubleInt: WeakConformance[Double, Int, Double] = new WeakConformance[Double, Int, Double] {
    implicit def aToC(a: Double) = a

    implicit def bToC(b: Int) = b
  }

  // More instances go here!


  def unify[A <: AnyVal, B <: AnyVal, C](a: A, b: B)(implicit ev: WeakConformance[A, B, C]): (C, C) = {
    import ev._
    (a: C, b: C)
  }
}

The method unify returns type C, which is figured out by the type inferencer based on availability of implicit values to provide as the implicit argument ev.

We can plug this into your wrapper class C as follows, also requiring a Numeric[WeakLub] so we can add the values.

case class C[A <: AnyVal](val value:A) {
  import WeakConformance.unify
  def +[B <: AnyVal, WeakLub <: AnyVal](that:C[B])(implicit wc: WeakConformance[A, B, WeakLub], num: Numeric[WeakLub]): C[WeakLub] = { 
    val w = unify(value, that.value) match { case (x, y) => num.plus(x, y)}; 
    new C[WeakLub](w)
  }
}

And finally, putting it all together:

object Test extends Application {
  val n = new C[Int](10)
  val d = new C[Double](10.5)

  // The type ascriptions aren't necessary, they are just here to 
  // prove the static type is the Weak LUB of the two sides.
  println(d + n: C[Double]) // C(20.5)
  println(n + n: C[Int])    // C(20)
  println(n + d: C[Double]) // C(20.5)
}

Test
retronym
Ahha! I see how this works - thank you! Adding a `Boolean` turned out to be easy, indeed, and the LUB of `Numeric` won't be too difficult to change for `Complex`.I'm curious - you seem to have had this solution handy up your sleeve, in what context did you come across this problem?Also I tried to test the performance of this solution and summing up a million `C[Int]`'s seems to be about five times slower than a million `Int`'s... Any thoughts on how to begin to optimize this?
ostolop
I played around this this during a discussion with @extempore on IRC, it wasn't solving a particular problem of mine. 5x overhead doesn't sound too bad considering the indirection. You could call `wc.a2b` and `wc.a2c` directly rather than using the `unify` method. Currently the inputs and output of `Numeric#plus` are boxed, hopefully a future version of Scala will find a way to @specialize that problem away.
retronym
@retronym Actually... I initiated that discussion. :-)
Daniel
@ostolop Retronym's solution came out of a discussion initiated by me regarding a related problem. My intent was taking a `List[AnyVal]` of numbers and performing a `foldLeft` on it. In the end, I gave up on that approach, as there was a much simpler solution to the _overall_ problem that avoided it altogether.
Daniel
And, in other news, that's why Scala is so cool. :-)
Daniel
Gotcha, I see where you came from. I can't tell yet if our overall problem will have a simpler solution: we are interpreting in Scala another language, which has a funny type system aimed at operating with data; it is almost wholly list-based, so all objects are list containers of some primitive types (int, double, character), which get converted among themselves fairly transparently to the user... Sounds familiar? :)
ostolop