¿Por qué no se compila el ejemplo, es decir, cómo funciona (co, contra e in varianza)?

147

Siguiendo con esta pregunta , ¿alguien puede explicar lo siguiente en Scala:

class Slot[+T] (var some: T) { 
   //  DOES NOT COMPILE 
   //  "COVARIANT parameter in CONTRAVARIANT position"

}

Entiendo la distinción entre +Ty Ten la declaración de tipo (se compila si la uso T). Pero entonces, ¿cómo se escribe realmente una clase que es covariante en su parámetro de tipo sin recurrir a crear la cosa sin parametrizar ? ¿Cómo puedo asegurarme de que lo siguiente solo se puede crear con una instancia de T?

class Slot[+T] (var some: Object){    
  def get() = { some.asInstanceOf[T] }
}

EDITAR : ahora esto se reduce a lo siguiente:

abstract class _Slot[+T, V <: T] (var some: V) {
    def getT() = { some }
}

Todo esto está bien, pero ahora tengo dos parámetros de tipo, donde solo quiero uno. Volveré a hacer la pregunta así:

¿Cómo puedo escribir una clase inmutable Slot que sea covariante en su tipo?

EDITAR 2 : ¡Duh! Yo solía vary no val. Lo siguiente es lo que quería:

class Slot[+T] (val some: T) { 
}
oxbow_lakes
fuente
66
Porque vares configurable mientras valque no lo es. Es la misma razón por la cual las colecciones inmutables de scala son covariantes, pero las mutables no lo son.
oxbow_lakes
Esto podría ser interesante en este contexto: scala-lang.org/old/node/129
user573215

Respuestas:

302

Genéricamente, un parámetro de tipo covariante es aquel que puede variar a medida que la clase se subtipea (alternativamente, varía según el subtipo, de ahí el prefijo "co-"). Más concretamente:

trait List[+A]

List[Int]es un subtipo de List[AnyVal]porque Intes un subtipo de AnyVal. Esto significa que puede proporcionar una instancia de List[Int]cuándo List[AnyVal]se espera un valor de tipo . Esta es realmente una forma muy intuitiva para que funcionen los genéricos, pero resulta que no es sólida (rompe el sistema de tipos) cuando se usa en presencia de datos mutables. Es por eso que los genéricos son invariantes en Java. Breve ejemplo de falta de solidez utilizando matrices Java (que son erróneamente covariantes):

Object[] arr = new Integer[1];
arr[0] = "Hello, there!";

Acabamos de asignar un valor de tipo Stringa una matriz de tipo Integer[]. Por razones que deberían ser obvias, estas son malas noticias. El sistema de tipos de Java realmente permite esto en tiempo de compilación. La JVM lanzará un "útilmente" ArrayStoreExceptionen tiempo de ejecución. El sistema de tipos de Scala previene este problema porque el parámetro de tipo en la Arrayclase es invariable (la declaración es [A]más que [+A]).

Tenga en cuenta que hay otro tipo de varianza conocida como contravarianza . Esto es muy importante ya que explica por qué la covarianza puede causar algunos problemas. La contravarianza es literalmente lo opuesto a la covarianza: los parámetros varían hacia arriba con el subtipo. Es mucho menos común en parte porque es muy intuitivo, aunque tiene una aplicación muy importante: las funciones.

trait Function1[-P, +R] {
  def apply(p: P): R
}

Observe la anotación de varianza " - " en el Pparámetro de tipo. Esta declaración en su conjunto significa que Function1es contravariante Py covariante en R. Por lo tanto, podemos derivar los siguientes axiomas:

T1' <: T1
T2 <: T2'
---------------------------------------- S-Fun
Function1[T1, T2] <: Function1[T1', T2']

Tenga en cuenta que T1'debe ser un subtipo (o el mismo tipo) de T1, mientras que es lo contrario para T2y T2'. En inglés, esto se puede leer de la siguiente manera:

Una función A es un subtipo de otra función B si el tipo de parámetro de A es un supertipo del tipo de parámetro de B mientras que el tipo de retorno de A es un subtipo del tipo de retorno de B .

La razón de esta regla se deja como un ejercicio para el lector (pista: piense en diferentes casos a medida que las funciones están subtipadas, como mi ejemplo de matriz de arriba).

Con su nuevo conocimiento de co y contravarianza, debería poder ver por qué el siguiente ejemplo no se compilará:

trait List[+A] {
  def cons(hd: A): List[A]
}

El problema es que Aes covariante, mientras que la consfunción espera que su parámetro de tipo sea invariante . Por lo tanto, Aestá variando la dirección equivocada. Curiosamente, podríamos resolver este problema haciendo Listcontravariante A, pero luego el tipo de retorno List[A]sería inválido ya que la consfunción espera que su tipo de retorno sea covariante .

Nuestras únicas dos opciones aquí son: a) hacer Ainvariante, perder las propiedades agradables e intuitivas de subtipo de covarianza, o b) agregar un parámetro de tipo local al consmétodo que se define Acomo un límite inferior:

def cons[B >: A](v: B): List[B]

Esto ahora es válido. Se puede imaginar que Aestá variando hacia abajo, pero Bes capaz de variar hacia arriba con respecto a Aya Aes su límite inferior. Con esta declaración de método, podemos Aser covariantes y todo funciona.

Tenga en cuenta que este truco solo funciona si devolvemos una instancia Listespecializada en el tipo menos específico B. Si intenta hacer Listmutable, las cosas se descomponen ya que termina tratando de asignar valores de tipo Ba una variable de tipo A, que el compilador no permite. Siempre que tenga mutabilidad, debe tener un mutador de algún tipo, que requiere un parámetro de método de cierto tipo, que (junto con el descriptor de acceso) implica invariancia. La covarianza funciona con datos inmutables ya que la única operación posible es un descriptor de acceso, al que se le puede dar un tipo de retorno covariante.

Daniel Spiewak
fuente
44
¿Podría expresarse esto en inglés simple como: puede tomar algo más simple como parámetro y puede devolver algo más complejo?
Phil
1
El compilador de Java (1.7.0) no compila "Object [] arr = new int [1];" sino que muestra el mensaje de error: "java: se requieren tipos incompatibles: java.lang.Object [] found: int []". Creo que quisiste decir "Object [] arr = new Integer [1];".
Emre Sevinç
2
Cuando mencionó, "El motivo de esta regla se deja como un ejercicio para el lector (pista: piense en diferentes casos a medida que las funciones están subtipadas, como mi ejemplo de matriz de arriba)". ¿Podría realmente dar un par de ejemplos?
perryzheng
2
@perryzheng por esto , toma trait Animal, trait Cow extends Animal, def iNeedACowHerder(herder: Cow => Unit, c: Cow) = herder(c)y def iNeedAnAnimalHerder(herder: Animal => Unit, a: Animal) = herder(a). Entonces, iNeedACowHerder({ a: Animal => println("I can herd any animal, including cows") }, new Cow {})está bien, ya que nuestro pastor de animales puede criar vacas, pero iNeedAnAnimalHerder({ c: Cow => println("I can herd only cows, not any animal") }, new Animal {})da un error de compilación, ya que nuestro pastor de vacas no puede criar a todos los animales.
Lasf
Esto está relacionado y me ayudó con la varianza: typelevel.org/blog/2016/02/04/variance-and-functors.html
Peter Schmitz
27

@Daniel lo ha explicado muy bien. Pero para explicarlo brevemente, si se permitiera:

  class Slot[+T](var some: T) {
    def get: T = some   
  }

  val slot: Slot[Dog] = new Slot[Dog](new Dog)   
  val slot2: Slot[Animal] = slot  //because of co-variance 
  slot2.some = new Animal   //legal as some is a var
  slot.get ??

slot.getluego arrojará un error en tiempo de ejecución ya que no se pudo convertir un AnimalaDog (¡duh!).

En general, la mutabilidad no va bien con la covarianza y la contravarianza. Esa es la razón por la cual todas las colecciones de Java son invariables.

Jatin
fuente
7

Ver Scala por ejemplo , página 57+ para una discusión completa de esto.

Si entiendo su comentario correctamente, debe volver a leer el pasaje que comienza en la parte inferior de la página 56 (básicamente, lo que creo que está pidiendo no es de tipo seguro sin verificaciones de tiempo de ejecución, que scala no hace, así que no tienes suerte). Traduciendo su ejemplo para usar su construcción:

val x = new Slot[String]("test") // Make a slot
val y: Slot[Any] = x             // Ok, 'cause String is a subtype of Any
y.set(new Rational(1, 2))        // Works, but now x.get() will blow up 

Si cree que no entiendo su pregunta (una posibilidad distinta), intente agregar más explicación / contexto a la descripción del problema e intentaré nuevamente.

En respuesta a su edición: las tragamonedas inmutables son una situación completamente diferente ... * sonrisa * Espero que el ejemplo anterior haya ayudado.

MarkusQ
fuente
He leído eso; desafortunadamente (todavía) no entiendo cómo puedo hacer lo que pido arriba (es decir, escribir una covariante de clase parametrizada en T)
oxbow_lakes
Eliminé mi downmark cuando me di cuenta de que esto era un poco duro. Debería haber dejado claro en la (s) pregunta (s) que había leído los bits de Scala por ejemplo; Solo quería que se explicara de una manera "menos formal"
oxbow_lakes
@oxbow_lakes smile Me temo que Scala By Example es la explicación menos formal. A lo sumo, podemos tratar de usar ejemplos concretos para el trabajo a pesar de que aquí ...
MarkusQ
Lo siento, no quiero que mi ranura sea mutable. Me acabo de dar cuenta de que el problema es que he declarado var y no val
oxbow_lakes
3

Debe aplicar un límite inferior en el parámetro. Me cuesta recordar la sintaxis, pero creo que se vería así:

class Slot[+T, V <: T](var some: V) {
  //blah
}

El Scala-by-example es un poco difícil de entender, algunos ejemplos concretos habrían ayudado.

Saem
fuente