Cara memahami variasi Scala dengan membina restoran

Saya faham bahawa varians jenis tidak penting untuk menulis kod Scala. Sudah lebih kurang setahun sejak saya menggunakan Scala untuk pekerjaan seharian saya, dan sejujurnya, saya tidak perlu terlalu risau.

Namun, saya fikir ia adalah topik "lanjutan" yang menarik, jadi saya mula mempelajarinya. Tidak mudah untuk memahaminya dengan segera, tetapi dengan contoh yang tepat, mungkin sedikit lebih mudah difahami. Izinkan saya mencuba menggunakan analogi berasaskan makanan ...

Apakah varians jenis?

Pertama sekali, kita harus menentukan apa jenis varians. Apabila anda berkembang dalam bahasa Berorientasikan Objek, anda dapat menentukan jenis kompleks. Itu bermaksud bahawa suatu jenis boleh diparamatisasi menggunakan jenis lain (jenis komponen).

Fikirkan Listmisalnya. Anda tidak dapat menentukan Listtanpa menentukan jenis mana yang ada di dalam senarai. Anda melakukannya dengan meletakkan jenis yang terkandung dalam senarai di dalam kurungan persegi: List[String]. Apabila anda menentukan jenis kompleks, anda dapat menentukan bagaimana ia akan mengubah hubungan subjenisnya mengikut hubungan antara jenis komponen dan subjenisnya.

Ok, kedengaran seperti huru-hara ... Mari praktikkan sedikit.

Membina empayar restoran

Matlamat kami adalah untuk membina empayar restoran. Kami mahukan restoran generik dan khusus. Setiap restoran yang akan kami buka memerlukan menu yang terdiri daripada pelbagai resipi, dan (mungkin) koki berbintang.

Resipi boleh terdiri daripada pelbagai jenis makanan (ikan, daging, daging putih, sayur-sayuran, dan lain-lain), sementara koki yang kami sewa mesti dapat memasak makanan semacam itu. Ini adalah model kami. Sekarang masanya pengekodan!

Jenis makanan yang berbeza

Sebagai contoh berasaskan makanan, kita mulakan dengan menentukan nama Trait Food, hanya memberikan nama makanan.

trait Food { def name: String } 

Kemudian kita dapat membuat Meatdan Vegetable, itu adalah subkelas dari Food.

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

Pada akhirnya, kami menentukan WhiteMeatkelas yang merupakan subkelas Meat.

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

Kedengarannya munasabah bukan? Oleh itu, kita mempunyai jenis hierarki ini.

hubungan subjenis makanan

Kita boleh membuat beberapa contoh makanan dari pelbagai jenis. Mereka akan menjadi ramuan resipi yang akan kami sajikan di restoran kami.

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

Resipi, jenis kovarian

Mari tentukan jenis kovarian Recipe. Ia memerlukan jenis komponen yang menyatakan makanan asas untuk resipi - iaitu resipi berdasarkan daging, sayur-sayuran, dll.

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

Ini Recipemempunyai nama dan senarai ramuan. Senarai ramuan mempunyai jenis yang sama Recipe. Untuk menyatakan bahawa itu Recipeadalah kovarian dalam jenisnya A, kami menulisnya sebagai Recipe[+A]. Resipi generik didasarkan pada setiap jenis makanan, resipi daging berdasarkan daging, dan resipi daging putih hanya mempunyai daging putih dalam senarai ramuannya.

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

Jenis adalah kovarian jika mengikut hubungan subtipe yang sama dengan jenis komponennya. Ini bermaksud Recipemengikuti hubungan subjenis yang sama dari komponennya.

hubungan subjenis resipi

Mari tentukan beberapa resipi yang akan menjadi sebahagian daripada menu yang berbeza.

// 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, jenis yang bertentangan

Kami menentukan beberapa resipi, tetapi kami memerlukan seorang tukang masak untuk memasaknya. Ini memberi kita peluang untuk bercakap mengenai kontravarian. Jenisnya bertentangan jika mengikuti hubungan terbalik subtipe jenis komponennya. Mari tentukan jenis kompleks kita Chef, yang bertentangan dengan jenis komponen. Jenis komponen akan menjadi makanan yang boleh dimasak oleh tukang masak.

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

A Chefmempunyai pengkhususan dan kaedah untuk memasak resipi berdasarkan makanan tertentu. Kami menyatakan bahawa bertentangan dengan menulisnya sebagai Chef[-A]. Sekarang kita boleh membuat seorang koki yang dapat memasak makanan generik, seorang tukang masak yang dapat memasak daging dan seorang koki yang khusus untuk daging putih.

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

Oleh kerana Chefbertentangan, Chef[Food]adalah subkelas Chef[Meat]yang merupakan subkelas dari Chef[WhiteMeat]. Ini bermaksud bahawa hubungan antara subtipe adalah kebalikan dari jenis komponennya Makanan.

hubungan subtipe chef

Baiklah, sekarang kita dapat menentukan chef yang berbeza dengan pelbagai pengkhususan untuk disewa di restoran kami.

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

Restoran, tempat bersatu

Kami mempunyai resipi, kami mempunyai koki, sekarang kami memerlukan restoran di mana koki dapat memasak menu resipi.

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

Kami tidak berminat dengan hubungan subjenis antara restoran, jadi kami dapat menentukannya sebagai tidak berubah. Jenis invarian tidak mengikuti hubungan antara subtipe jenis komponen. Dengan kata lain, Restaurant[Food]bukan subkelas atau superclass Restaurant[Meat]. Mereka tidak berkaitan.

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) 

Perkara yang sama berlaku MeatRestaurantdan WhiteMeatRestaurant. Saya boleh menyampaikan contoh hanya menu yang terdiri daripada resipi yang lebih spesifik daripada yang diperlukan, tetapi koki yang dapat memasak makanan lebih generik daripada yang diperlukan.

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) 

Itu sahaja, empayar restoran kami siap menjana banyak wang!

Kesimpulannya

Baiklah, dalam kisah ini, saya melakukan yang terbaik untuk menjelaskan perbezaan jenis di Scala. Ini adalah topik lanjutan, tetapi perlu diketahui kerana ingin tahu. Saya harap contoh restoran dapat membantu menjadikannya lebih mudah difahami. Sekiranya ada sesuatu yang tidak jelas, atau jika saya menulis sesuatu yang salah (saya masih belajar!) Jangan ragu untuk memberikan komen!

Jumpa! ?