Як зрозуміти розбіжності Scala, будуючи ресторани

Я розумію, що дисперсія типу не є принциповою для написання коду Scala. Минув більш-менш рік, відколи я використовую Scala для своєї повсякденної роботи, і, чесно кажучи, мені ніколи не доводилося над цим сильно турбуватися.

Однак я думаю, що це цікава "просунута" тема, тому я почав її вивчати. Нелегко зрозуміти це відразу, але на правильному прикладі це може бути трохи легше зрозуміти. Дозвольте спробувати застосувати аналогію на основі їжі ...

Що таке дисперсія типу?

Перш за все, ми повинні визначити, що таке дисперсія типу. Коли ви розробляєте на об’єктно-орієнтованій мові, ви можете визначити складні типи. Це означає, що тип може бути параметризований за допомогою іншого типу (тип компонента).

Подумайте, Listнаприклад. Ви не можете визначити a, Listне вказавши, які типи будуть у списку. Ви робите це, встановлюючи тип , що міститься в списку всередині квадратних дужок: List[String]. Коли ви визначаєте складний тип, ви можете вказати, як він буде змінювати відносини свого підтипу відповідно до співвідношення між типом компонента та його підтипами.

Добре, звучить як безлад ... Давайте трохи практичні.

Побудова ресторанної імперії

Наша мета - побудувати імперію ресторанів. Ми хочемо загальних та спеціалізованих ресторанів. У кожному ресторані, який ми відкриємо, потрібне меню, яке складається з різних рецептів, і (можливо) зірковий кухар.

Рецепти можуть складатися з різних видів їжі (риби, м’яса, білого м’яса, овочів тощо), тоді як найнятий нами кухар повинен вміти готувати такий вид їжі. Це наша модель. Тепер час кодування!

Різні типи їжі

Для нашого прикладу, заснованого на їжі, ми починаємо з визначення Trait Food, надаючи лише назву їжі.

trait Food { def name: String } 

Тоді ми можемо створити Meatі Vegetable, що є підкласами Food.

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

Врешті-решт, ми визначаємо WhiteMeatклас, який є підкласом Meat.

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

Звучить розумно, правда? Отже, у нас така ієрархія типів.

відносини харчового підтипу

Ми можемо створити деякі харчові екземпляри різного типу. Вони будуть інгредієнтами рецептів, які ми будемо подавати в наших ресторанах.

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

Рецепт, коваріантний тип

Давайте визначимо коваріантний тип Recipe. Потрібен компонентний тип, який виражає базову їжу для рецепта - тобто рецепт на основі м’яса, овочів тощо.

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

RecipeМає ім'я і список інгредієнтів. Список інгредієнтів має однаковий тип Recipe. Щоб виразити, що Recipeковаріантний за своїм типом A, ми записуємо його як Recipe[+A]. Загальний рецепт базується на кожному виді їжі, рецепт м’яса ґрунтується на м’ясі, а рецепт білого м’яса містить лише біле м’ясо у своєму списку інгредієнтів.

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

Тип є коваріантним, якщо слідує однаковому відношенню підтипів типу його компонента. Це означає, що Recipeслід той самий підтип взаємозв'язку його компонента Їжа.

відносини підтипу рецепта

Давайте визначимо кілька рецептів, які будуть частиною різних меню.

// 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, який є противаріантним у типі компонента. Тип компонента буде їжею, яку кухар може приготувати.

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

A Chefмає спеціалізацію та спосіб приготування рецепту на основі певної їжі. Ми виражаємо, що це противаріантно, записуючи це як Chef[-A]. Тепер ми можемо створити шеф-кухаря, здатного готувати загальну їжу, шеф-кухаря, здатного готувати м’ясо, та шеф-кухаря, який спеціалізується на білому м’ясі.

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

Оскільки Chefє противаріантною, Chef[Food]є підкласом Chef[Meat], тобто підкласом Chef[WhiteMeat]. Це означає, що взаємозв'язок між підтипами є зворотною до його компонента типу Їжа.

відносини підтипу шеф-кухаря

Гаразд, тепер ми можемо визначити різних шеф-кухарів з різною спеціалізацією для найму в наших ресторанах.

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

Ресторан, де все поєднується

У нас є рецепти, у нас є кухарі, тепер нам потрібен ресторан, де шеф-кухар може приготувати меню рецептів.

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

Нас не цікавить взаємозв'язок підтипів між ресторанами, тому ми можемо визначити його як інваріантний. Інваріантний тип не відповідає взаємозв'язку між підтипами типу компонента. Іншими словами, Restaurant[Food]не є підкласом чи суперкласом Restaurant[Meat]. Вони просто не пов’язані між собою.

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) 

Те саме стосується MeatRestaurantі WhiteMeatRestaurant. Я можу передати екземпляру лише меню, яке складається з більш конкретних рецептів, ніж необхідний, але кухарі, які можуть готувати їжу більш загальну, ніж потрібна.

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) 

Ось і все, наша імперія ресторанів готова заробляти кучу грошей!

Висновок

Добре, хлопці, у цій історії я зробив все можливе, щоб пояснити відхилення типу в Scala. Це просунута тема, але це варто знати просто з цікавості. Я сподіваюся, що приклад ресторану може допомогти зробити його зрозумілішим. Якщо щось незрозуміле, або якщо я написав щось не так (я все ще вчуся!), Не соромтеся залишати коментар!

Побачимося! ?