2011-09-08 8 views
9

Me gustaría definir un rasgo con algunas propiedades que tienen una relación bien definida, por ejemplo, digamos que a * b = c. La idea es que las implementaciones de este rasgo pueden proporcionar dos de estos y tener un descriptor para la tercera propiedad derivada automáticamente.Inferir implementaciones de métodos predeterminados mutuamente dependientes en Scala

( Esta es la misma característica que las clases de tipos de Haskell, si no recuerdo aquellos correctamente, en donde si ha definido < de Ord, >= se ejecutaría de la ! . < - aunque se puede definir cualquier subconjunto de funciones, siempre que el resto . podría inferirse) (no recuerdo las clases de tipos de Haskell correctamente)

El enfoque ingenuo realmente funciona bastante bien:.

trait Foo { 
    // a * b = c 
    def a: Double = c/b 
    def b: Double = c/a 
    def c: Double = a * b 
} 

class FooOne(override val a: Double, override val b: Double) extends Foo 
class FooTwo(override val a: Double, override val c: Double) extends Foo 

Aquí, las implementaciones FooOne y FooTwo son implementaciones completas de Foo, y se comportan como se esperaba. Hasta aquí todo bien; este enfoque permite a las clases definir dos de las propiedades y obtener la tercera "gratis".

Sin embargo, las cosas empiezan a parecer menos optimista si se define una tercera clase:

class FooFail(override val a: Double) extends Foo 

Esto compila bien - sin embargo, se provocará un desbordamiento de pila si alguna vez se evalúan sus bc o métodos.


Así que el enfoque ingenuo da la inferencia aspecto del enfoque de clase de tipos de Haskell, pero no tenemos la seguridad de tiempo de compilación. Lo que me gustaría es que el compilador se queje si se implementan menos de dos de los métodos implementando clases. Claramente, la sintaxis actual no es suficiente aquí; necesitamos que los métodos se consideren abstractos, aunque con una implementación predeterminada que se puede usar si y solo si los métodos dependientes no son abstractos.

¿Scala expone la semántica adecuada para definir esto? (No tengo ningún problema si hay una forma indirecta de definirlo, similar a union types, ya que no tengo conocimiento de ningún soporte de primera clase para esto en el lenguaje).

Si no, me las arreglaré con el enfoque ingenuo y simplemente definiré y probaré mis clases con cuidado. Pero realmente creo que esto es algo que el sistema de tipos debería poder atrapar (después de todo, no es Ruby. :)).

+2

A menos que olvide mal por completo, Haskell utiliza precisamente el mismo enfoque ingenuo con el mismo problema. –

+0

@Alexey - comentario interesante. Ha pasado un tiempo, pero me parece recordar que si no definía los métodos suficientes, obtendría un error de compilador/intérprete según el cual no era posible derivar todos los métodos para la clase de texto. Tal vez intente reproducir esto y leer sobre cómo lo hace Haskell, en busca de inspiración. –

+0

OK, después de haber probado esto en Haskell, de hecho sufre el mismo problema (es equivalente a la situación de Scala ya que solo advierte sobre métodos totalmente abstractos). Dicho eso, la pregunta sigue en pie, aunque sin el precedente en otro idioma. –

Respuesta

8

Use implicits:

object test { 
    case class A(v : Double) 
    case class B(v : Double) 
    case class C(v : Double) 

    implicit def a(implicit b : B, c : C) = A(c.v/b.v) 
    implicit def b(implicit a : A, c : C) = B(c.v/a.v) 
    implicit def c(implicit a : A, b : B) = C(a.v * b.v) 

    def foo(implicit a : A, b : B, c : C) = 
    a + ", " + b + ", " + c 

    // Remove either of these and it won't compile 
    implicit val aa = A(3) 
    implicit val bb = B(4) 

    def main(args : Array[String]) { 
    println(foo) 
    } 
} 
+0

Enfoque muy interesante: según mi comentario a didierd, sospecho que las implícitas serán clave para que el compilador compruebe que existen definiciones suficientes. Es una pena que los consumidores no puedan simplemente declarar 'val b = 5' más. Me pregunto si esto se puede masajear a través de otra capa interna de rasgos. –

5

¿Cómo se obtiene la seguridad en Haskell? No estoy muy familiarizado con el idioma, pero no puedo hacer

data Func = Func (Int -> Int) 
instance Eq Func 
instance Ord Func 

compared = Func (\i -> i-1) < Func (\i -> i+1) 

y obtener un desbordamiento de pila cuando se evalúa en comparación.

Puedo imaginar una solución en scala pero bastante débil. En primer lugar, dejar de Foo totalmente abstracta

trait Foo { def a: Double; def b: Double; def c: Double } 

A continuación, crear rasgos mixin para cada combinación método de definición permitido

trait FooWithAB extends Foo {def c : Double = a * b} 
trait FooWithAC extends Foo {def b : Double = c/a} 
trait FooWithBC extends Foo {def a : Double = c/b} 

FooOne y FooTwo tendrá que mezclar en uno de los rasgos.En FooFail, nada le impide mezclarse en dos de ellos, y aún falla, pero ha sido advertido de alguna manera.

Es es posible ir un paso más allá y prohibir la mezcla de dos de ellos, con una especie de tipo fantasma

trait Foo { 
    type t; 
    def a: Double; def b: Double; def c: Double 
} 
trait FooWithAB extends Foo {type t = FooWithAB; def c : Double = a * b} 
trait FooWithAC extends Foo {type t = FooWithAC; def b : Double = c/a} 
trait FooWithBC extends Foo {type t = FooWithBC; def c : Double = c/b} 

Este evitar que se mezclen dos de los FooWithXX, no se puede definir

class FooFail(val a: Double) extends FooWithAC with FooWithAB 
+0

Buen punto: sospecho que cualquier solución implicará dejar los métodos totalmente abstractos en 'Foo', y luego mezclar las definiciones predeterminadas de * somewhere *. El enfoque de tipo fantasma es interesante, aunque es una pena que el clunkiness esté expuesto a los consumidores que tienen que mezclar explícitamente el rasgo. Tal vez las conversiones implícitas podrían solucionar esto de alguna manera, mmm ... –

2

Una solución ligeramente más débil que lo que es posible que desee es la siguiente:

trait Foo { 
    def a : Double 
    def b : Double 
    def c : Double 
} 

// Anything with two out of three can be promoted to Foo status. 
implicit def ab2Foo(ab : { def a : Double; def b : Double }) = 
    new Foo { val a = ab.a; val b = ab.b; def c = ab.a * ab.b } 
implicit def bc2Foo(bc : { def b : Double; def c : Double }) = 
    new Foo { val a = bc.c/bc.b; val b = bc.b; def c = bc.c } 
implicit def ac2Foo(ac : { def a : Double; def c : Double }) = 
    new Foo { val a = ac.a; val b = ac.c/ac.a; def c = ac.c } 

Aquí, cualquier clase con dos de los tres a, b, c métodos puede ser visto y utilizado como Foo. Por ejemplo:

case class AB(a : Double, b : Double) 
AB(5.0, 7.1).c // prints 35.5 

Pero si se intenta por ejemplo:

case class ABC(a : Double, b : Double, c : Double) 
val x : Foo = ABC(1.0, 2.0, 3.0) 

... se obtiene un error de "ambigua implícita".

El problema principal de su declaración original es que las clases no heredan correctamente de Foo, lo que puede ser un problema si desea reutilizar otros métodos. Sin embargo, puede solucionarlo teniendo otro atributo FooImpl que contenga estos otros métodos y restringir las conversiones implícitas a los subtipos de FooImpl.

+0

Me gusta lo que ha hecho aquí, utilizando tipos estructurales junto con conversiones implícitas para realizar el control en tiempo de compilación, sin poner ningún requisito adicional en los consumidores. Estoy de acuerdo en que no ser un 'Foo' no es ideal, pero me temo que puede ser inevitable para obtener los otros requisitos (de lo contrario, los métodos en 'Foo' tienen que ser simultáneamente abstractos y definidos). –

Cuestiones relacionadas