2010-06-21 13 views
9

Me gustaría implementar una clase C para almacenar valores de varios tipos numéricos, así como booleanos. Además, me gustaría ser capaz de operar en instancias de esta clase, entre los tipos, la conversión en caso necesario Int --> Double y Boolean -> Int, es decir, para ser capaz de añadir Boolean + Boolean, Int + Boolean, Boolean + Int, Int + Double, Double + Double etc., devolviendo el más pequeño posible escriba (Int o Double) siempre que sea posible.¿Cómo configurar la conversión implícita para permitir la aritmética entre los tipos numéricos?

Hasta el momento se me ocurrió esto:

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 
} 

Esto funciona para algunos casos (1, 2, 3), pero no hace por (4, 5). La razón es que hay un ensanchamiento implícito del tipo de menor a mayor, pero no a la inversa. En cierto modo, el método

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

alguna manera tiene que tener un método socio que sería algo como:

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

pero eso no compila por dos razones, en primer lugar, que el compilador no puede convertir this.n a tipo T (aunque especificamos view bound A <% T), y, en segundo lugar, que incluso si fuera capaz de convertir this.n, después de escribir el borrado, los dos métodos + se vuelven ambiguos.

Disculpa, esto es muy largo. ¡Cualquier ayuda sería muy apreciada! De lo contrario, parece que tengo que escribir todas las operaciones entre todos los tipos explícitamente. Y se pondría peludo si tuviera que agregar tipos adicionales (Complex es el siguiente en el menú ...).

¿Quizás alguien tenga otra manera de lograr todo esto por completo? Siente que hay algo simple que estoy pasando por alto.

¡Gracias de antemano!

Respuesta

6

Bien, Daniel!

He restringido la solución para ignorar Boolean, y solo trabajo con AnyVals que tienen un límite mínimo débil que tiene una instancia de Numeric. Estas restricciones son arbitrarias, puede eliminarlas y codificar su propia relación de conformidad débil entre los tipos: la implementación de a2b y a2c podría realizar alguna conversión.

Su interesante considerar cómo los parámetros implícitos puede simular la herencia (el paso de parámetros implícitos del tipo (calculado => Base) o de conformidad débil. Son muy potente, sobre todo cuando el tipo de inferencer le ayuda a cabo.

En primer lugar, necesitamos una clase de tipo para representar el límite superior débil débil de todos los pares de tipos A y B que nos interesen.

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) 
    } 
} 

El método unify Devuelve el tipo C, que se descubrió por el inferencer tipo basado en la disponibilidad de valores implícitos para proporcionar como argumento implícito ev.

Podemos conectar esto en su envoltura clase C de la siguiente manera, también requiere un Numeric[WeakLub] para que podamos agregar los valores.

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) 
    } 
} 

Y, por último, poner todo junto:

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 
+0

Ahha! Veo cómo funciona esto, ¡gracias! Agregar un 'Boolean' resultó ser fácil, de hecho, y el LUB de' Numérico' no será demasiado difícil de cambiar para 'Complex'. Tengo curiosidad: parece que esta solución te ha sido muy útil, ¿en qué contexto te encontraste con este problema? También traté de probar el rendimiento de esta solución y resumir un millón 'C [Int]' s parece ser unas cinco veces más lento que un millón 'Int' ... Cualquier idea sobre cómo comenzar a optimizar esto? – ostolop

+0

Jugué alrededor de esto durante una discusión con @extempore en IRC, no estaba resolviendo un problema particular mío. La sobrecarga de 5x no suena tan mal teniendo en cuenta la indirección. Puede llamar a 'wc.a2b' y' wc.a2c' directamente en lugar de usar el método 'unify'. Actualmente las entradas y salidas de 'NumeriC# plus' están encasilladas, con suerte una versión futura de Scala encontrará la manera de @specializar ese problema. – retronym

+0

@retronym En realidad ... inicié esa discusión. :-) –

3

Hay un way para hacer eso, pero lo dejo en retronym para explicarlo, ya que él escribió esta solución. :-)

Cuestiones relacionadas