Comment comprendre les variations de Scala en construisant des restaurants

Je comprends que la variance de type n'est pas fondamentale pour écrire du code Scala. Cela fait plus ou moins un an que j'utilise Scala pour mon travail quotidien, et honnêtement, je n'ai jamais eu à m'inquiéter beaucoup à ce sujet.

Cependant, je pense que c'est un sujet "avancé" intéressant, alors j'ai commencé à l'étudier. Il n'est pas facile de le saisir immédiatement, mais avec le bon exemple, cela pourrait être un peu plus facile à comprendre. Laissez-moi essayer d'utiliser une analogie alimentaire ...

Qu'est-ce que la variance de type?

Tout d'abord, nous devons définir ce qu'est la variance de type. Lorsque vous développez dans un langage orienté objet, vous pouvez définir des types complexes. Cela signifie qu'un type peut être paramétré à l'aide d'un autre type (type de composant).

Pensez Listpar exemple. Vous ne pouvez pas définir a Listsans spécifier les types qui figureront dans la liste. Vous le faites en mettant le type contenu dans la liste entre crochets: List[String]. Lorsque vous définissez un type complexe, vous pouvez spécifier comment il fera varier sa relation de sous-type en fonction de la relation entre le type de composant et ses sous-types.

Ok, ça ressemble à un gâchis ... Prenons un peu de pratique.

Construire un empire de la restauration

Notre objectif est de construire un empire de restaurants. Nous voulons des restaurants génériques et spécialisés. Chaque restaurant que nous ouvrirons a besoin d'un menu composé de différentes recettes et d'un chef (éventuellement) étoilé.

Les recettes peuvent être composées de différents types d'aliments (poisson, viande, viande blanche, légumes, etc.), tandis que le chef que nous embauchons doit être capable de cuisiner ce type d'aliments. Ceci est notre modèle. Il est maintenant temps de coder!

Différents types de nourriture

Pour notre exemple basé sur l'alimentation, nous commençons par définir le Trait Food, en fournissant uniquement le nom de l'aliment.

trait Food { def name: String } 

Ensuite, nous pouvons créer Meatet Vegetable, qui sont des sous-classes de Food.

class Meat(val name: String) extends Food 
class Vegetable(val name: String) extends Food 

En fin de compte, nous définissons une WhiteMeatclasse qui est une sous-classe de Meat.

class WhiteMeat(override val name: String) extends Meat(name) 

Cela semble raisonnable, non? Nous avons donc cette hiérarchie de types.

relation de sous-type alimentaire

Nous pouvons créer des instances alimentaires de différents types. Ce seront les ingrédients des recettes que nous allons servir dans nos restaurants.

// Food <- Meat val beef = new Meat("beef") // Food <- Meat <- WhiteMeat val chicken = new WhiteMeat("chicken") val turkey = new WhiteMeat("turkey") // Food <- Vegetable val carrot = new Vegetable("carrot") val pumpkin = new Vegetable("pumpkin") 

Recette, un type covariant

Définissons le type covariant Recipe. Il faut un type de composant qui exprime l'aliment de base de la recette - c'est-à-dire une recette à base de viande, de légumes, etc.

trait Recipe[+A] { def name: String def ingredients: List[A] } 

Le Recipea un nom et une liste d'ingrédients. La liste des ingrédients a le même type de Recipe. Pour exprimer que le Recipeest covariant dans son type A, nous l'écrivons comme Recipe[+A]. La recette générique est basée sur tous les types d'aliments, la recette de viande est basée sur la viande et une recette de viande blanche ne contient que de la viande blanche dans sa liste d'ingrédients.

case class GenericRecipe(ingredients: List[Food]) extends Recipe[Food] { def name: String = s"Generic recipe based on ${ingredients.map(_.name)}" } 
case class MeatRecipe(ingredients: List[Meat]) extends Recipe[Meat] { def name: String = s"Meat recipe based on ${ingredients.map(_.name)}" } 
case class WhiteMeatRecipe(ingredients: List[WhiteMeat]) extends Recipe[WhiteMeat] { def name: String = s"Meat recipe based on ${ingredients.map(_.name)}" } 

Un type est covariant s'il suit la même relation de sous-types de son type de composant. Cela signifie que Recipesuit la même relation de sous-type de son composant Aliment.

relation de sous-type de recette

Définissons quelques recettes qui feront partie de différents menus.

// Recipe[Food]: Based on Meat or Vegetable val mixRecipe = new GenericRecipe(List(chicken, carrot, beef, pumpkin)) // Recipe[Food] <- Recipe[Meat]: Based on any kind of Meat val meatRecipe = new MeatRecipe(List(beef, turkey)) // Recipe[Food] <- Recipe[Meat] <- Recipe[WhiteMeat]: Based only on WhiteMeat val whiteMeatRecipe = new WhiteMeatRecipe(List(chicken, turkey)) 

Chef, un type contravariant

Nous avons défini certaines recettes, mais nous avons besoin d'un chef pour les cuisiner. Cela nous donne l'occasion de parler de contravariance. Un type est contravariant s'il suit une relation inverse des sous-types de son type de composant. Définissons notre type complexe Chef, qui est contravariant dans le type de composant. Le type de composant sera la nourriture que le chef peut cuisiner.

trait Chef[-A] { def specialization: String def cook(recipe: Recipe[A]): String } 

A Chefa une spécialisation et une méthode pour cuisiner une recette basée sur un aliment spécifique. Nous exprimons qu'il est contravariant de l'écrire comme Chef[-A]. Nous pouvons désormais créer un chef capable de cuisiner des aliments génériques, un chef capable de cuisiner de la viande et un chef spécialisé dans la viande blanche.

class GenericChef extends Chef[Food] { val specialization = "All food" override def cook(recipe: Recipe[Food]): String = s"I made a ${recipe.name}" } 
class MeatChef extends Chef[Meat] { val specialization = "Meat" override def cook(recipe: Recipe[Meat]): String = s"I made a ${recipe.name}" } 
class WhiteMeatChef extends Chef[WhiteMeat] { override val specialization = "White meat" def cook(recipe: Recipe[WhiteMeat]): String = s"I made a ${recipe.name}" } 

Puisque Chefest contravariant, Chef[Food]est une sous-classe de Chef[Meat]qui est une sous-classe de Chef[WhiteMeat]. Cela signifie que la relation entre les sous-types est l'inverse de son type de composant Aliment.

relation de sous-type de chef

Ok, nous pouvons maintenant définir différents chefs avec diverses spécialisations à embaucher dans nos restaurants.

// Chef[WhiteMeat]: Can cook only WhiteMeat val giuseppe = new WhiteMeatChef giuseppe.cook(whiteMeatRecipe) // Chef[WhiteMeat] <- Chef[Meat]: Can cook only Meat val alfredo = new MeatChef alfredo.cook(meatRecipe) alfredo.cook(whiteMeatRecipe) // Chef[WhiteMeat]<- Chef[Meat] <- Chef[Food]: Can cook any Food val mario = new GenericChef mario.cook(mixRecipe) mario.cook(meatRecipe) mario.cook(whiteMeatRecipe) 

Restaurant, où les choses se rencontrent

Nous avons des recettes, nous avons des chefs, maintenant nous avons besoin d'un restaurant où le chef peut cuisiner un menu de recettes.

trait Restaurant[A] { def menu: List[Recipe[A]] def chef: Chef[A] def cookMenu: List[String] = menu.map(chef.cook) } 

Nous ne sommes pas intéressés par la relation de sous-type entre les restaurants, nous pouvons donc la définir comme invariante. Un type invariant ne suit pas la relation entre les sous-types du type de composant. En d'autres termes, Restaurant[Food]n'est pas une sous-classe ou une superclasse de Restaurant[Meat]. Ils ne sont tout simplement pas liés.

We will have a GenericRestaurant, where you can eat different type of food. The MeatRestaurant is specialised in meat-based dished and the WhiteMeatRestaurant is specialised only in dishes based on white meat. Every restaurant to be instantiated needs a menu, that is a list of recipes, and a chef able to cook the recipes in the menu. Here is where the subtype relationship of Recipe and Chef comes into play.

case class GenericRestaurant(menu: List[Recipe[Food]], chef: Chef[Food]) extends Restaurant[Food] 
case class MeatRestaurant(menu: List[Recipe[Meat]], chef: Chef[Meat]) extends Restaurant[Meat] 
case class WhiteMeatRestaurant(menu: List[Recipe[WhiteMeat]], chef: Chef[WhiteMeat]) extends Restaurant[WhiteMeat] 

Let's start defining some generic restaurants. In a generic restaurant, the menu is composed of recipes of various type of food. Since Recipe is covariant, a GenericRecipe is a superclass of MeatRecipe and WhiteMeatRecipe, so I can pass them to my GenericRestaurant instance. The thing is different for the chef. If the Restaurant requires a chef that can cook generic food, I cannot put in it a chef able to cook only a specific one. The class Chef is covariant, so GenericChef is a subclass of MeatChef that is a subclass of WhiteMeatChef. This implies that I cannot pass to my instance anything different from GenericChef.

val allFood = new GenericRestaurant(List(mixRecipe), mario) val foodParadise = new GenericRestaurant(List(meatRecipe), mario) val superFood = new GenericRestaurant(List(whiteMeatRecipe), mario) 

Il en va de même pour MeatRestaurantet WhiteMeatRestaurant. Je ne peux passer à l'instance qu'un menu composé de recettes plus spécifiques que celle requise, mais des chefs qui peuvent cuisiner des aliments plus génériques que celui requis.

val meat4All = new MeatRestaurant(List(meatRecipe), alfredo) val meetMyMeat = new MeatRestaurant(List(whiteMeatRecipe), mario) 
val notOnlyChicken = new WhiteMeatRestaurant(List(whiteMeatRecipe), giuseppe) val whiteIsGood = new WhiteMeatRestaurant(List(whiteMeatRecipe), alfredo) val wingsLovers = new WhiteMeatRestaurant(List(whiteMeatRecipe), mario) 

Ca y est, notre empire de restaurants est prêt à gagner des tonnes d'argent!

Conclusion

Ok les gars, dans cette histoire, j'ai fait de mon mieux pour expliquer les variations de type dans Scala. C'est un sujet avancé, mais il vaut la peine de le savoir simplement par curiosité. J'espère que l'exemple du restaurant pourra être utile pour le rendre plus compréhensible. Si quelque chose n'est pas clair, ou si j'ai écrit quelque chose de mal (j'apprends encore!), N'hésitez pas à laisser un commentaire!

À plus! ?