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

StackOverflow https://stackoverflow.com/questions/663254

Question

Après cette question , quelqu'un peut-il expliquer ce qui suit en 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 (elle est compilée si j'utilise Slot ). . Mais alors, comment écrit-on réellement 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 var ?

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

MODIFIER - réduisez-le à l’affichage suivant:

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

tout cela est bon, mais j’ai maintenant deux paramètres de type, où je n’en veux qu’un. Je vais poser à nouveau la question:

Comment puis-je écrire une classe immuable val qui est covariante dans son type?

MODIFIER 2 : Duh! J'ai utilisé <=> et non <=>. Voici ce que je voulais:

class Slot[+T] (val some: T) { 
}
Était-ce utile?

La solution

Généralement, un paramètre de type covariant est un paramètre qui peut varier à mesure que la classe est sous-typée (ou varie avec le sous-typage, d’où le & "; co - &"; préfixe). Plus concrètement:

trait List[+A]

List[Int] est un sous-type de List[AnyVal] car Int est un sous-type de AnyVal. Cela signifie que vous pouvez fournir une instance de String lorsqu'une valeur de type Integer[] est attendue. C’est vraiment un moyen très intuitif pour les génériques de fonctionner, mais il s’avère qu’il est peu sain (casse le système de typage) lorsqu’il est utilisé en présence de données mutables. C'est pourquoi les génériques sont invariants en Java. Petit exemple de manque d’utilisation des tableaux Java (covariants par erreur):

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

Nous venons d'attribuer une valeur de type ArrayStoreException à un tableau de type Array. Pour des raisons qui devraient être évidentes, c'est une mauvaise nouvelle. Le système de types de Java permet en fait cela au moment de la compilation. La machine virtuelle Java & "Servira utilement &"; lance un [A] au moment de l'exécution. Le système de types de Scala évite ce problème car le paramètre type de la [+A] classe est invariant (la déclaration est P plutôt que Function1).

Notez qu’il existe un autre type de variance appelé contravariance . Ceci est très important car cela explique pourquoi la covariance peut causer des problèmes. La covariance 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 tellement contre-intuitif, bien qu’il ait une application très importante: les fonctions.

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

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

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

Notez que T2' doit être un sous-type (ou le même type) de A, alors que c'est l'inverse pour cons et List. 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 de A est un sur-type du type de paramètre de B alors que le type de retour de A est un sous-type du type de retour de B .

La raison de cette règle est laissée au lecteur sous forme d'exercice (astuce: réfléchissez à différents cas lorsque les fonctions sont sous-typées, comme dans l'exemple de tableau ci-dessus).

Avec vos nouvelles connaissances sur les co- et contravariances, vous devriez être en mesure de comprendre pourquoi l'exemple suivant ne compilera pas:

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

Le problème est que List[A] est covariant, alors que la fonction B s'attend à ce que son paramètre de type soit invariant . Ainsi, <=> varie la mauvaise direction. Il est intéressant de noter que nous pourrions résoudre ce problème en rendant <=> contravariant dans <=>, mais le type de retour <=> serait alors invalide car la fonction <=> s'attend à ce que son type de retour soit covariant . .

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

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

Ceci est maintenant valide. Vous pouvez imaginer que <=> varie vers le bas, mais <=> peut varier vers le haut par rapport à <=> puisque <=> est sa limite inférieure. Avec cette déclaration de méthode, nous pouvons avoir <=> être covariant et tout se passe bien.

Notez que cette astuce ne fonctionne que si nous renvoyons une instance de <=> spécialisée sur le type moins spécifique <=>. Si vous essayez de rendre <=> mutable, les choses ne fonctionnent plus car vous finissez par essayer d'attribuer des valeurs de type <=> à une variable de type <=>, qui n'est pas autorisée par le compilateur. Chaque fois que vous avez une mutabilité, vous devez avoir un mutateur quelconque, qui nécessite un paramètre de méthode d'un certain type, qui (avec l'accesseur) implique une invariance. 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.

Autres conseils

@Daniel l'a très bien expliqué. Mais pour expliquer en un mot, 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 émettra alors une erreur au moment de l'exécution car la conversion d'un Animal en Dog (duh!) n'a pas abouti.

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 sont invariantes.

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

Si je comprends bien votre commentaire, vous devez relire le passage qui commence au bas de la page 56 (en gros, ce que vous demandez, je pense, n’est pas digne de sécurité sans vérification de la durée d’exécution, ce que scala ne fait pas. t faire, alors vous êtes hors 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 davantage d'explications / de contextes à la description du problème et je réessayerai.

En réponse à votre modification: les créneaux horaires immuables constituent une situation totalement différente ... * sourire * J'espère que l'exemple ci-dessus vous a aidé.

Vous devez appliquer une limite inférieure au paramètre. J'ai de la difficulté à me souvenir de la syntaxe, mais je pense que cela ressemblerait à ceci:

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

Le Scala par exemple est un peu difficile à comprendre, quelques exemples concrets auraient aidé.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top