web-dev-qa-db-fra.com

Pourquoi l'exemple ne compile-t-il pas, comment fonctionne la (co-, contre- et in-) variance?

Suite à cette question , quelqu'un peut-il expliquer ce qui suit dans Scala:

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

}

Je comprends la distinction entre +T et T dans la déclaration de type (il compile si j'utilise T). Mais alors comment peut-on réellement écrire une classe qui est covariante dans son paramètre de type sans avoir recours à la création de la chose non paramétrée? Comment puis-je m'assurer que les éléments suivants ne peuvent être créés qu'avec une instance de T?

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

[~ # ~] modifier [~ # ~] - maintenant ceci se résume à ce qui suit:

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

tout cela est bien, mais j'ai maintenant deux paramètres de type, où je n'en veux qu'un. Je pose de nouveau la question ainsi:

Comment puis-je écrire une classe immuableSlot qui est covariant dans son type?

EDIT 2 : Duh! J'ai utilisé var et non val. Voici ce que je voulais:

class Slot[+T] (val some: T) { 
}
146
oxbow_lakes

De manière générique, un paramètre de type covariant est celui qui peut varier à mesure que la classe est sous-typée (alternativement, varie avec le sous-typage, d'où le préfixe "co-"). Plus concrètement:

trait List[+A]

List[Int] est un sous-type de List[AnyVal] parce que Int est un sous-type de AnyVal. Cela signifie que vous pouvez fournir une instance de List[Int] lorsqu'une valeur de type List[AnyVal] devrait. C'est vraiment un moyen très intuitif pour les génériques de fonctionner, mais il s'avère qu'il n'est pas sain (casse le système de type) lorsqu'il est utilisé en présence de données mutables. C'est pourquoi les génériques sont invariants en Java. Bref exemple de non-validité en utilisant Java (qui sont covariants par erreur):

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

Nous venons d'affecter une valeur de type String à un tableau de type Integer[]. Pour des raisons qui devraient être évidentes, ce sont de mauvaises nouvelles. Le système de types de Java permet cela au moment de la compilation. La JVM lancera "utilement" un ArrayStoreException au moment de l'exécution. Le système de types de Scala évite ce problème car le paramètre type de la classe Array est invariant (la déclaration est [A] plutôt que [+A]).

Notez qu'il existe un autre type de variance appelé contravariance. Ceci est très important car il explique pourquoi la covariance peut causer certains problèmes. La contravariance est littéralement l'opposé de la covariance: les paramètres varient vers le haut avec le sous-typage. C'est beaucoup moins courant en partie parce qu'il est si contre-intuitif, bien qu'il ait une application très importante: les fonctions.

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

Notez l'annotation de variance "-" sur le paramètre de type P. Cette déclaration dans son ensemble signifie que Function1 est contravariant dans P et covariant dans R. Ainsi, nous pouvons dériver les axiomes suivants:

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

Remarquerez que T1' doit être un sous-type (ou le même type) de T1, alors que c'est l'inverse pour T2 et T2'. En anglais, cela peut être lu comme suit:

Une fonction [~ # ~] a [~ # ~] est un sous-type d'une autre fonction [~ # ~] b [~ # ~] si le type de paramètre [~ # ~] a [~ # ~] est un supertype du type de paramètre [~ # ~] b [~ # ~] = alors que le type de retour [~ # ~] a [~ # ~] est un sous-type du type de retour [~ # ~] b [~ # ~].

La raison de cette règle est laissée au lecteur comme exercice (indice: pensez à différents cas car les fonctions sont sous-typées, comme mon exemple de tableau ci-dessus).

Avec vos nouvelles connaissances en matière de co- et de contravariance, vous devriez pouvoir comprendre pourquoi l'exemple suivant ne se compilera pas:

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

Le problème est que A est covariant, tandis que la fonction cons s'attend à ce que son paramètre de type soit invariant. Ainsi, A varie dans la mauvaise direction. Chose intéressante, nous pourrions résoudre ce problème en rendant List contravariant dans A, mais le type de retour List[A] ne serait pas valide car la fonction cons s'attend à ce que son type de retour soit covariant.

Nos deux seules options ici sont de a) rendre A invariant, en perdant les propriétés de sous-typage Nice et intuitives de la covariance, ou b) ajouter un paramètre de type local à la méthode cons qui définit A comme borne inférieure:

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

Ceci est maintenant valide. Vous pouvez imaginer que A varie vers le bas, mais B est capable de varier vers le haut par rapport à A puisque A est sa limite inférieure. Avec cette déclaration de méthode, nous pouvons avoir A être covariant et tout fonctionne.

Notez que cette astuce ne fonctionne que si nous renvoyons une instance de List qui est spécialisée sur le type moins spécifique B. Si vous essayez de rendre List mutable, les choses tombent en panne puisque vous finissez par essayer d'attribuer des valeurs de type B à une variable de type A, ce qui n'est pas autorisé par le compilateur . Chaque fois que vous avez une mutabilité, vous devez avoir un mutateur d'une certaine sorte, ce qui nécessite un paramètre de méthode d'un certain type, qui (avec l'accesseur) implique l'invariance. La covariance fonctionne avec des données immuables car la seule opération possible est un accesseur, auquel on peut attribuer un type de retour covariant.

298
Daniel Spiewak

@Daniel l'a très bien expliqué. Mais pour l'expliquer en bref, si cela était autorisé:

  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.get générera alors une erreur lors de l'exécution car il n'a pas réussi à convertir un Animal en Dog (duh!).

En général, la mutabilité ne va pas bien avec la co-variance et la contre-variance. C'est la raison pour laquelle toutes les collections Java Java sont invariantes.

27
Jatin

Voir Scala par exemple , page 57+ pour une discussion complète à ce sujet.

Si je comprends bien votre commentaire, vous devez relire le passage commençant au bas de la page 56 (en gros, ce que je pense que vous demandez n'est pas sûr pour le type sans vérification de l'exécution, qui scala ne fait pas, donc vous n'avez pas de chance.) Traduire leur exemple pour utiliser votre construction:

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 vous pensez que je ne comprends pas votre question (une possibilité distincte), essayez d'ajouter plus d'explications/de contexte à la description du problème et je vais réessayer.

En réponse à votre montage: les emplacements immuables sont une toute autre situation ... * sourire * J'espère que l'exemple ci-dessus a aidé.

7
MarkusQ

Vous devez appliquer une limite inférieure sur le paramètre. J'ai du mal à me souvenir de la syntaxe, mais je pense que cela ressemblerait à quelque chose comme ceci:

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

Le Scala-by-example est un peu difficile à comprendre, quelques exemples concrets auraient aidé.

3
Saem