La notion de variance est une notion souvent énigmatique pour les développeurs. On retrouve cette notion dès lors que nous sommes en présence d'un langage utilisant à la fois du sous-typage et des types paramétrés (ou type générique). Ce qui inclut une bonne partie des langages proposant un style de programmation orienté objet basé sur des classes et parmi eux, les langages qui utilisent les génériques (Java, C#, Scala...).

Si j'ai écrit énigmatique, c'est bien parce qu'à travers la notion de variance nous nous retrouvons parfois avec des erreurs de compilation difficiles à comprendre. Il faudra alors jouer avec T extends U ou ? super T en Java. Et en Scala, ce sera avec les expressions +A, -A, A <: B et A >: B.

Pourtant le choix est relativement simple : sommes-nous en position de producteur / fournisseur de données ou en position de consommateur ? De cette question, vous pourrez déterminer si un paramètre est covariant ou contravariant.

Covariance

La cas le plus souvent rencontré et celui qui est le plus intuitif est la covariance. Ce cas apparaît avec les collections, avec le type Option, mais aussi avec le type IO et les types représentant des fonctions (le type de la sortie d'une fonction est covariant). La covariance s'applique dès qu'il est possible d'utiliser l'analogie avec une boîte, un conteneur, un burrito ou tout ce qui s'apparente à de la production ou à de la fourniture de données.

En exemple, nous allons créer une arborescence représentant des aliments, des fruits et des légumes (classification purement culinaire, tout en s'évitant le cas de la tomate).

trait Aliment {
  def name: String
}
case class Fruit(name: String) extends Aliment
case class Legume(name: String) extends Aliment

val fraise = Fruit("fraise")
val brocoli = Legume("brocoli")

Nous avons maintenant deux gourmands : Mary qui mange plutôt varié et Johnny qui ne mange que des fruits. Représentons les par des fonctions.

def maryMange(l: List[Aliment]): Unit = ()
def johnnyMange(l: List[Fruit]): Unit = ()

Dans ce cas, passer une liste de fruits à ces deux fonctions ne posent pas problème. Le cas de Johnny est trivial. Pour Mary, il n'y a pas d'inconvénient puisque les fruits sont des aliments.

val fruits: List[Fruit] = List(fraise, fraise)

maryMange(fruits)   // OK 😻 - grâce à la covariance, cela compile !
johnnyMange(fruits) // OK 👌 - ça compile !

Par contre :

val aliments: List[Aliment] = List(brocoli, fraise)

maryMange(aliments)   // OK 👌 - ça compile !
johnnyMange(aliments) // NOK 🙀 - erreur de compilation

Nous avons alors une erreur de compilation, puisque Johnny ne peut/veut pas manger n'importe quel aliment.

List[A] est covariant puisque le type de List varie dans le même sens que le type de A. Puisque Fruit est un sous-type de Aliment, alors List[Fruit] est un sous-type de List[Aliment]. Ça fonctionne avec les types Stream[A], Option[A], RDD[A], Parser[A], X => A (uniquement si nous nous intéressons à la variance de A), Either[E, A]...

De manière générale, nous pouvons imaginer un type Producteur[A] sur lequel nous avons une méthode get permettant de récupérer une valeur. Nous allons utiliser le symbole + en prefix de la définition du paramètre de type pour indiquer que le paramètre de type est covariant.

case class Producteur[+A](get: A)

val jardinFraise: Producteur[Fruit] = Producteur(fraise)
val jardinAliment: Producteur[Aliment] = Producteur(brocoli)

def recolte[A](producteur: Producteur[A]): A = producteur.get

recolte[Aliment](jardinFraise)  // OK 😻 - grâce à la covariance, cela compile !
recolte[Aliment](jardinAliment) // OK 👌 - ça compile !

recolte[Fruit](jardinFraise)    // OK 👌 - ça compile !
recolte[Fruit](jardinAliment)   // NOK 🙀 - erreur de compilation

Contravariance

Alors là, accrochez-vous. La contravariance est pour le coup moins intuitif.

Nous avons vu que la covariance apparaît lorsqu'un type générique varie dans le même sens que son type sous-jacent. Et bien, il y a des cas où le type générique varie dans le sens inverse par rapport à son type sous-jacent. Autrement dit, on a bien Fruit qui est sous-type de Aliment, mais DownUnder[Aliment] est un sous-type DownUnder[Fruit], si DownUnder est un tel type.