Πώς να κατανοήσετε τις διακυμάνσεις της Σκάλας δημιουργώντας εστιατόρια

Κατανοώ ότι η διακύμανση τύπου δεν είναι θεμελιώδης για τη σύνταξη κώδικα Scala. Έχουν περάσει περισσότερο από ένα χρόνο από τότε που χρησιμοποίησα τη Scala για την καθημερινή μου δουλειά, και ειλικρινά, δεν είχα ποτέ να ανησυχώ πολύ για αυτό.

Ωστόσο, νομίζω ότι είναι ένα ενδιαφέρον "προηγμένο" θέμα, οπότε άρχισα να το μελετώ. Δεν είναι εύκολο να το καταλάβεις αμέσως, αλλά με το σωστό παράδειγμα, μπορεί να είναι λίγο πιο εύκολο να το καταλάβεις. Επιτρέψτε μου να δοκιμάσω να χρησιμοποιήσω μια αναλογία με βάση τα τρόφιμα ...

Τι είναι η διακύμανση τύπου;

Πρώτα απ 'όλα, πρέπει να καθορίσουμε τι είναι η διακύμανση τύπου. Όταν αναπτύσσεστε σε μια αντικειμενοστρεφή γλώσσα, μπορείτε να ορίσετε σύνθετους τύπους. Αυτό σημαίνει ότι ένας τύπος μπορεί να παραμεριστεί χρησιμοποιώντας έναν άλλο τύπο (τύπος συστατικού).

Σκεφτείτε Listγια παράδειγμα. Δεν μπορείτε να ορίσετε ένα 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ακολουθεί την ίδια σχέση υποτύπου του συστατικού του Food.

σχέση υποτύπου συνταγής

Ας καθορίσουμε μερικές συνταγές που θα είναι μέρος διαφορετικών μενού.

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

Α 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]. Αυτό σημαίνει ότι η σχέση μεταξύ των υποτύπων είναι το αντίστροφο του συστατικού του τύπου Food.

σχέση υποτύπου σεφ

Εντάξει, μπορούμε τώρα να ορίσουμε διαφορετικό σεφ με διάφορες εξειδικεύσεις για ενοικίαση στα εστιατόριά μας.

// 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]δεν είναι υποκατηγορία ή superclass της 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. Είναι ένα προχωρημένο θέμα, αλλά αξίζει να γνωρίζετε μόνο από την περιέργεια. Ελπίζω ότι το παράδειγμα του εστιατορίου μπορεί να σας βοηθήσει να το καταστήσετε πιο κατανοητό. Εάν κάτι δεν είναι σαφές ή αν έγραψα κάτι λάθος (μαθαίνω ακόμα!) Μη διστάσετε να αφήσετε ένα σχόλιο!

Τα λέμε! ;