Cómo comprender las variaciones de Scala mediante la construcción de restaurantes

Entiendo que la variación de tipo no es fundamental para escribir código Scala. Ha pasado más o menos un año desde que utilizo Scala para mi trabajo diario y, sinceramente, nunca he tenido que preocuparme mucho por eso.

Sin embargo, creo que es un tema "avanzado" interesante, así que comencé a estudiarlo. No es fácil comprenderlo de inmediato, pero con el ejemplo correcto, podría ser un poco más fácil de entender. Permítanme intentar usar una analogía basada en alimentos ...

¿Qué es la variación de tipo?

En primer lugar, tenemos que definir qué tipo de varianza es. Cuando desarrolla en un lenguaje orientado a objetos, puede definir tipos complejos. Eso significa que un tipo se puede parametrizar usando otro tipo (tipo de componente).

Piense en, Listpor ejemplo. No puede definir un Listsin especificar qué tipos estarán dentro de la lista. Lo hace poniendo el tipo de contenido en la lista entre corchetes: List[String]. Cuando define un tipo complejo, puede especificar cómo variará su relación de subtipo de acuerdo con la relación entre el tipo de componente y sus subtipos.

Ok, suena como un desastre ... Seamos un poco prácticos.

Construyendo un imperio de restaurantes

Nuestro objetivo es construir un imperio de restaurantes. Queremos restaurantes genéricos y especializados. Cada restaurante que abriremos necesita un menú compuesto por diferentes recetas y un chef (posiblemente) estrella.

Las recetas pueden estar compuestas por diferentes tipos de alimentos (pescado, carne, carnes blancas, verduras, etc.), mientras que el chef que contratemos tiene que ser capaz de cocinar ese tipo de alimentos. Este es nuestro modelo. ¡Ahora es el momento de codificar!

Diferentes tipos de comida

Para nuestro ejemplo basado en alimentos, comenzamos definiendo el Trait Food, proporcionando solo el nombre del alimento.

trait Food { def name: String } 

Entonces podemos crear Meaty Vegetable, que son subclases de Food.

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

Al final, definimos una WhiteMeatclase que es una subclase de Meat.

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

Suena razonable, ¿verdad? Entonces tenemos esta jerarquía de tipos.

relación de subtipos de alimentos

Podemos crear algunas instancias de comida de varios tipos. Serán los ingredientes de las recetas que vamos a servir en nuestros restaurantes.

// 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") 

Receta, un tipo covariante

Definamos el tipo covariante Recipe. Se necesita un tipo de componente que exprese el alimento base de la receta, es decir, una receta a base de carne, verdura, etc.

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

El Recipetiene un nombre y una lista de ingredientes. La lista de ingredientes tiene el mismo tipo de Recipe. Para expresar que Recipees covariante en su tipo A, lo escribimos como Recipe[+A]. La receta genérica se basa en todo tipo de alimentos, la receta de carne se basa en carne y una receta de carne blanca tiene solo carne blanca en su lista de ingredientes.

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 tipo es covariante si sigue la misma relación de subtipos de su tipo de componente. Esto significa que Recipesigue la misma relación de subtipo de su componente Alimentos.

relación de subtipo de receta

Definamos algunas recetas que formarán parte de distintos menús.

// 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 tipo contravariante

Definimos algunas recetas, pero necesitamos un chef para cocinarlas. Esto nos da la oportunidad de hablar sobre la contravarianza. Un tipo es contravariante si sigue una relación inversa de subtipos de su tipo de componente. Definamos nuestro tipo complejo Chef, que es contravariante en el tipo de componente. El tipo de componente será la comida que el chef pueda cocinar.

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

A Cheftiene una especialización y un método para cocinar una receta basada en un alimento específico. Expresamos que es contravariante escribirlo como Chef[-A]. Ahora podemos crear un chef capaz de cocinar alimentos genéricos, un chef capaz de cocinar carne y un chef especializado en carnes blancas.

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}" } 

Dado que Chefes contravariante, Chef[Food]es una subclase de Chef[Meat]que es una subclase de Chef[WhiteMeat]. Esto significa que la relación entre subtipos es la inversa de su componente tipo Alimentos.

relación de subtipo chef

Ok, ahora podemos definir diferentes chefs con diversas especializaciones para contratar en nuestros restaurantes.

// 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) 

Restaurante, donde las cosas se juntan

Tenemos recetas, tenemos chefs, ahora necesitamos un restaurante donde el chef pueda cocinar un menú de recetas.

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

No nos interesa la relación de subtipos entre restaurantes, por lo que podemos definirla como invariante. Un tipo invariante no sigue la relación entre los subtipos del tipo de componente. En otras palabras, Restaurant[Food]no es una subclase o superclase de Restaurant[Meat]. Simplemente no están relacionados.

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) 

Lo mismo ocurre con MeatRestauranty WhiteMeatRestaurant. Puedo pasar a la instancia solo un menú compuesto por recetas más específicas que la requerida, pero chefs que pueden cocinar alimentos más genéricos que los requeridos.

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) 

Eso es todo, ¡nuestro imperio de restaurantes está listo para ganar mucho dinero!

Conclusión

Bien, chicos, en esta historia hice todo lo posible para explicar las variaciones de tipo en Scala. Es un tema avanzado, pero vale la pena conocerlo solo por curiosidad. Espero que el ejemplo del restaurante pueda ser de ayuda para hacerlo más comprensible. Si algo no está claro, o si escribí algo mal (¡todavía estoy aprendiendo!) ¡No dudes en dejar un comentario!

¡Nos vemos! ?