I understand that type variance is not fundamental to writing Scala code. It's been more or less a year since I've been using Scala for my day-to-day job, and honestly, I've never had to worry much about it.
我了解类型差异并不是编写Scala代码的基础。 自从我在日常工作中使用Scala以来,已经差不多一年了,说实话,我从来没有为此担心过。
However, I think it is an interesting "advanced" topic, so I started to study it. It is not easy to grasp it immediately, but with the right example, it might be a little bit easier to understand. Let me try using a food-based analogy...
但是,我认为这是一个有趣的“高级”主题,因此我开始研究它。 立即掌握它并不容易,但是有了正确的例子,可能会更容易理解。 让我尝试使用基于食物的类比...
什么是类型差异? (What is type variance?)
First of all, we have to define what type variance is. When you develop in an Object-Oriented language, you can define complex types. That means that a type may be parametrized using another type (component type).
首先,我们必须定义什么类型差异。 使用面向对象的语言进行开发时,可以定义复杂的类型。 这意味着可以使用另一个类型(组件类型)对一个类型进行参数化。
Think of List
for example. You cannot define a List
without specifying which types will be inside the list. You do it by putting the type contained in the list inside square brackets: List[String]
. When you define a complex type, you can specify how it will vary its subtype relationship according to the relation between the component type and its subtypes.
例如以List
为例。 你不能定义List
没有指定哪些类型将是名单内。 您可以通过将列表中包含的类型放在方括号内来实现: List[String]
。 定义复杂类型时,可以指定如何根据组件类型与其子类型之间的关系来更改其子类型关系。
Ok, sounds like a mess... Let's get a little practical.
好吧,听起来像是一团糟...让我们实际一点。
建立餐厅帝国 (Building a restaurant empire)
Our goal is to build an empire of restaurants. We want generic and specialised restaurants. Every restaurant we will open needs a menu composed of different recipes, and a (possibly) starred chef.
我们的目标是建立餐厅帝国。 我们想要普通和专门的餐厅。 我们将要开设的每家餐厅都需要一个菜单,菜单中包含不同的食谱,以及一位(可能是)主厨。
The recipes can be composed of different kinds of food (fish, meat, white meat, vegetables, etc.), while the chef we hire has to be able to cook that kind of food. This is our model. Now it's coding time!
食谱可以由不同种类的食物(鱼,肉,白肉,蔬菜等)组成,而我们雇用的厨师必须能够烹饪这种食物。 这是我们的模型。 现在是编码时间!
不同种类的食物 (Different types of food)
For our food-based example, we start by defining the Trait Food
, providing just the name of the food.
对于以食物为基础的示例,我们首先定义Trait Food
,仅提供Trait Food
名称。
trait Food {def name: String}
Then we can create Meat
and Vegetable
, that are subclasses of Food
.
然后我们可以创建Meat
和Vegetable
,它们是Food
子类。
class Meat(val name: String) extends Food
class Vegetable(val name: String) extends Food
In the end, we define a WhiteMeat
class that is a subclass of Meat
.
最后,我们定义了WhiteMeat
类,它是Meat
的子类。
class WhiteMeat(override val name: String) extends Meat(name)
Sounds reasonable right? So we have this hierarchy of types.
听起来合理吧? 因此,我们具有这种类型的层次结构。
We can create some food instances of various type. They will be the ingredients of the recipes we are going to serve in our 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")
配方,协变类型 (Recipe, a covariant type)
Let's define the covariant type Recipe
. It takes a component type that expresses the base food for the recipe - that is, a recipe based on meat, vegetable, etc.
让我们定义协变类型Recipe
。 它采用表示配方基本食物的成分类型-即基于肉,蔬菜等的配方。
trait Recipe[+A] {def name: Stringdef ingredients: List[A]}
The Recipe
has a name and a list of ingredients. The list of ingredients has the same type of Recipe
. To express that the Recipe
is covariant in its type A
, we write it as Recipe[+A]
. The generic recipe is based on every kind of food, the meat recipe is based on meat, and a white meat recipe has just white meat in its list of ingredients.
Recipe
有名称和成分清单。 配料表具有相同的Recipe
类型。 为了表示Recipe
在其类型A
是协变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)}"}
A type is covariant if it follows the same relationship of subtypes of its component type. This means that Recipe
follows the same subtype relationship of its component Food.
如果类型遵循其组件类型的子类型的相同关系,则该类型是协变的。 这意味着, Recipe
遵循其组成食品的相同子类型关系。
Let's define some recipes that will be part of different 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, a contravariant type)
We defined some recipes, but we need a chef to cook them. This gives us the chance to talk about contravariance. A type is contravariant if it follows an inverse relationship of subtypes of its component type. Let's define our complex type Chef
, that is contravariant in the component type. The component type will be the food that the chef can cook.
我们定义了一些食谱,但我们需要一名厨师来烹饪。 这使我们有机会谈论自变量。 如果类型遵循其组件类型的子类型的逆关系,则该类型是协变的。 让我们定义复杂类型Chef
,它与组件类型相反。 成分类型将是厨师可以烹饪的食物。
trait Chef[-A] {def specialization: Stringdef cook(recipe: Recipe[A]): String
}
A Chef
has a specialisation and a method to cook a recipe based on a specific food. We express that it is contravariant writing it as Chef[-A]
. Now we can create a chef able to cook generic food, a chef able to cook meat and a chef specialised on white meat.
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}"
}
Since Chef
is contravariant, Chef[Food]
is a subclass of Chef[Meat]
that is a subclass of Chef[WhiteMeat]
. This means that the relationship between subtypes is the inverse of its component type Food.
由于Chef
是协变的,因此Chef[Food]
是Chef[Meat]
的子类,而后者是Chef[WhiteMeat]
的子类。 这意味着子类型之间的关系与其组件类型Food相反。
Ok, we can now define different chef with various specialization to hire in our 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, where things come together)
We have recipes, we have chefs, now we need a restaurant where the chef can cook a menu of recipes.
我们有食谱,我们有厨师,现在我们需要一家餐厅,厨师可以在这里烹饪菜单。
trait Restaurant[A] {def menu: List[Recipe[A]]def chef: Chef[A]def cookMenu: List[String] = menu.map(chef.cook)
}
We are not interested in the subtype relationship between restaurants, so we can define it as invariant. An invariant type does not follow the relationship between the subtypes of the component type. In other words, Restaurant[Food]
is not a subclass or superclass of Restaurant[Meat]
. They are simply unrelated. 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.
我们对餐厅之间的子类型关系不感兴趣,因此可以将其定义为不变的。 不变类型不遵循组件类型的子类型之间的关系。 换句话说, Restaurant[Food]
不是Restaurant[Meat]
的子类或超类。 它们根本无关。 我们将设有GenericRestaurant
,您可以在这里吃不同类型的食物。 MeatRestaurant
专供肉类菜肴, WhiteMeatRestaurant
仅专供WhiteMeatRestaurant
菜肴。 每个要实例化的餐厅都需要一个菜单,该菜单是食谱列表,并且厨师可以在菜单中烹饪食谱。 这是Recipe
和Chef
的子类型关系起作用的地方。
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
.
让我们开始定义一些通用餐厅。 在一家普通餐厅中,菜单由各种食物的食谱组成。 由于Recipe
是协变的,因此GenericRecipe
是MeatRecipe
和WhiteMeatRecipe
的超类,因此我可以将它们传递给GenericRestaurant
实例。 对于厨师而言,情况有所不同。 如果餐厅需要一位可以烹制普通食品的厨师,那么我不能放入只能烹制特定食品的厨师。 类Chef
是协变的,所以GenericChef
是的子类MeatChef
是的子类WhiteMeatChef
。 这意味着我不能将与GenericChef
不同的任何东西传递给我的实例。
val allFood = new GenericRestaurant(List(mixRecipe), mario)
val foodParadise = new GenericRestaurant(List(meatRecipe), mario)
val superFood = new GenericRestaurant(List(whiteMeatRecipe), mario)
The same goes for MeatRestaurant
and WhiteMeatRestaurant
. I can pass to the instance only a menu composed of more specific recipes then the required one, but chefs that can cook food more generic than the required one.
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)
That's it, our empire of restaurants is ready to make tons of money!
就是这样,我们的餐厅帝国已准备好赚很多钱!
结论 (Conclusion)
Ok guys, in this story I did my best to explain type variances in Scala. It is an advanced topic, but it is worth to know just out of curiosity. I hope that the restaurant example can be of help to make it more understandable. If something is not clear, or if I wrote something wrong (I'm still learning!) don't hesitate to leave a comment!
好的,在这个故事中,我尽力解释了Scala中的类型差异。 这是一个高级主题,但是出于好奇,值得了解。 我希望餐馆的例子可以帮助使它更容易理解。 如果不清楚,或者我写错了什么(我还在学习!),请随时发表评论!
See you! ?
再见! ?
翻译自: https://www.freecodecamp.org/news/understand-scala-variances-building-restaurants/