Διαδρομές
/
Elm
Elm
/
Ασκήσεις
/
Εργοστάσιο θησαυρών
Εργοστάσιο θησαυρών

Εργοστάσιο θησαυρών

Άσκηση εκμάθησης

Εισαγωγή

Τύποι-φαντάσματα

Οι τύποι της Elm επιτρέπουν γενικούς τύπους, που γενικά προσθέτουν ευελιξία σε μια διεπαφή. Για παράδειγμα, ο τύπος Maybe a μπορεί να περιέχει μια τιμή οποιουδήποτε τύπου.

type Maybe a = Nothing | Just a

Σημείωσε ότι στο παραπάνω παράδειγμα, η παράμετρος τύπου a χρησιμοποιείται και στις δύο πλευρές του συμβόλου ίσον =. Λέμε ότι το a είναι δεσμευμένο με κάποια δεδομένα μέσα στον ορισμό του τύπου.

Η τεχνική του τύπου-φάντασμα

Σε ορισμένες περιπτώσεις όμως, η παράμετρος τύπου εμφανίζεται μόνο στην αριστερή πλευρά της εξίσωσης.

type Distance unit = Distance Float

Στον παραπάνω ορισμό του Distance, το unit είναι μια ελεύθερη παράμετρος τύπου, που δεν είναι δεσμευμένη με κανένα δεδομένο του τύπου. Μπορούμε επίσης να αποκαλέσουμε αυτόν τον τύπο τύπο-φάντασμα.

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

-- Distance is an opaque type, since the module does not expose its variants.
-- This means that users may only use the functions meter, foot and add to manipulate distances.
module Distance exposing (Distance, Meter, Foot, meter, foot, add)

-- The Distance type has a phantom type 'unit'.
type Distance unit = Distance Float

-- We define two types that will be used in place
-- of the phantom type 'unit' in our constructor functions.
type Meter = Meter
type Foot = Foot

-- Constructor for Meter
meter : Distance Meter
meter = Distance 1.0

-- Constructor for Foot
foot : Distance Foot
foot = Distance 0.3048

-- The add function cannot take two parameters of different types.
-- So we cannot add meters and feet by mistake.
add : Distance unit -> Distance unit -> Distance unit
add (Distance d1) (Distance d2) = Distance (d1 + d2)

Ο κώδικας έξω από αυτό το module δεν μπορεί να προσπελάσει τα εσωτερικά του τύπου Distance. Οι χρήστες μπορούν να προσθέσουν δύο Distance Meter, αλλά δεν μπορούν να προσθέσουν ένα Distance Meter με ένα Distance Foot.

import Distance exposing (Distance)

-- Compiles
twoMeters = Distance.add Distance.meter Distance.meter

-- Does not compile
errDist = Distance.add Distance.meter Distance.foot

Επεκτάσιμες καταγραφές ως τύποι-φαντάσματα

Δεν υπάρχει περιορισμός στο ποιοι τύποι μπορούν να είναι τύποι-φαντάσματα, και οι τύποι καταγραφής μπορούν επίσης να χρησιμοποιηθούν. Όταν χρησιμοποιούνται ως τύποι-φαντάσματα, οι καταγραφές μπορούν να εκφράσουν σύνθετους περιορισμούς που μετασχηματίζονται μέσα από συναρτήσεις.

Ας προσθέσουμε μια νέα μονάδα απόστασης μέσα στο module Distance, με το όνομα LegoBlock. Τα φυσικά τουβλάκια, όντας τυποποιημένα αντικείμενα, έχουν πάντα δύο ιδιότητες: έχουν μη κλασματικές και μη αρνητικές αποστάσεις.

type LegoBlock = LegoBlock

fourStuds : Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
fourStuds = Distance 4.0

Τα nonFractional και nonNegative είναι τα πεδία μιας καταγραφής που δεν υπάρχει έξω από ένα όρισμα τύπου, οπότε ταιριάζει να τους δώσουμε τον τύπο (), που ονομάζεται τύπος μονάδας και δεν κρατά καμία πληροφορία πέρα από το ότι υπάρχει.

Το να αποκτήσουμε αυθαίρετες αποστάσεις LegoBlock είναι φυσικά δυνατό, για παράδειγμα αφού υπολογίσουμε διαφορές ή λόγους αποστάσεων.

negativeStud : Distance { properties | unit: LegoBlock, nonFractional : () }
negativeStud = Distance -1.0

threeFiddyStud : Distance { properties | unit: LegoBlock, nonNegative : () }
threeFiddyStud = Distance 3.50

crazyStud : Distance { properties | unit: LegoBlock }
crazyStud = Distance -13.37

Όλες οι παραπάνω τιμές είναι έγκυρες και θα μεταγλωττιστούν, ωστόσο έχουμε ιδιαίτερο ενδιαφέρον για τιμές όπως το fourStuds που έχουν και τις δύο ιδιότητες nonFractional και nonNegative, γιατί αντιπροσωπεύουν φυσικά τουβλάκια που μπορούν να συνδυαστούν με

combineLegoBlocks
  :  Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
  -> Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
  -> Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
combineLegoBlocks = add

Ας προσθέσουμε μερικές συναρτήσεις ώστε οι χρήστες να μπορούν να δημιουργούν και να βελτιώνουν αποστάσεις LegoBlock.

newLegoBlock : Float -> Distance { properties | unit: LegoBlock }
newLegoBlock dist = Distance dist

floorDistance : Distance properties -> Distance { properties | nonFractional : () }
floorDistance (Distance dist) = Distance (toFloat (floor dist))

ceilingDistance : Distance properties -> Distance { properties | nonFractional : () }
ceilingDistance (Distance dist) = Distance (toFloat (ceiling dist))

absDistance : Distance properties -> Distance { properties | nonNegative : () }
absDistance (Distance dist) = Distance (abs dist)

Σημείωσε ότι οι floorDistance, ceilingDistance και absDistance μπορούν να χειριστούν και μονάδες διαφορετικές από το LegoBlock, και γενικά δεν κάνουν καμία υπόθεση για τις ιδιότητες εισόδου, απλώς εγγυώνται ότι η έξοδος θα έχει μια συγκεκριμένη ιδιότητα, είτε nonFractional είτε nonNegative.

Ας δούμε μερικά αποτελέσματα.

import Distance exposing (Distance)

-- Compiles
distance1 = combineLegoBlocks fourStuds fourStuds

-- Does not compile
distance2 = combineLegoBlocks fourStuds threeFiddyStud

-- Compiles
distance3 = combineLegoBlocks fourStuds (floorDistance threeFiddyStud)

-- Does not compile
distance4 = combineLegoBlocks fourStuds negativeStud

-- Compiles
distance5 = combineLegoBlocks fourStuds (absDistance negativeStud)

-- Does not compile
distance6 = combineLegoBlocks fourStuds crazyStud

-- Compiles
distance7 = combineLegoBlocks fourStuds (floorDistance (absDistance crazyStud))

-- Compiles
distance8 = combineLegoBlocks fourStuds (ceilingDistance (absDistance crazyStud))

Γενικά, οι floorDistance και ceilingDistance παρέχουν διαφορετικά αποτελέσματα, αλλά τις ίδιες εγγυήσεις. Αυτή είναι η δύναμη της τεχνικής του τύπου-φάντασμα: παρέχει ευέλικτες επιλογές στους χρήστες, διατηρώντας παράλληλα ισχυρές εγγυήσεις.

Οδηγίες

Εσύ, ο Άρχοντας του Κακού, είσαι πολύ περήφανος για την ποιότητα των TreasureChest που βρίσκονται στα μπουντρούμια του Κακού σου σε όλο τον κόσμο. Δεν είναι εύκολο να κρατήσεις την ποιότητα ψηλά: οι διευθυντές των μπουντρουμιών σου συνεχώς τα κάνουν θάλασσα στο φτιάξιμο θησαυρών, οπότε αποφασίζεις να τους δώσεις ένα Elm API για να τους υποτάξεις στο θέλημά σου.

Υπάρχουν δύο προϋποθέσεις στις οποίες δεν υποχωρείς:

  1. ένα TreasureChest πρέπει να προστατεύεται από έναν ασφαλή κωδικό πρόσβασης τουλάχιστον 8 χαρακτήρων
  2. σε ένα συγκεκριμένο μπουντρούμι, κάθε TreasureChest πρέπει να περιέχει έναν μοναδικό θησαυρό

Οι διευθυντές των μπουντρουμιών σου θα προτείνουν μια λίστα με προτάσεις κωδικών/θησαυρών, και μόνο τα κατάλληλα TreasureChest θα δημιουργηθούν.

Με αυτά τα κριτήρια, υπάρχουν δύο πιθανοί τρόποι να δημιουργήσεις ασφαλή σεντούκια από μια λίστα προτάσεων:

  1. αφαίρεσε αυτά με μη ασφαλείς κωδικούς, και μετά αφαίρεσε αυτά με διπλότυπους θησαυρούς
  2. αφαίρεσε αυτά με διπλότυπους θησαυρούς, και μετά αφαίρεσε αυτά με μη ασφαλείς κωδικούς

Αυτοί οι δύο μπορεί να μη δώσουν τα ίδια αποτελέσματα (για παράδειγμα για [("strong_password", GoldStatue), ("1234", GoldStatue)]), αλλά δε σε πειράζει είτε έτσι είτε αλλιώς, οπότε θέλεις να αφήσεις τους διευθυντές των μπουντρουμιών σου να αποφασίσουν.

Ένα API που αφήνει στους χρήστες κάποια επιλογή, αλλά εξακολουθεί να εγγυάται ιδιότητες του τελικού αποτελέσματος; Ακούγεται σαν ιδανική περίπτωση για την τεχνική του phantom type!

1. Δώσε ένα υπόδειγμα για τα σεντούκια θησαυρού

Ο τύπος TreasureChest και ο συνοδός του getTreasure δίνονται ήδη, αλλά πρέπει να σκεφτείς έναν τύπο για μια πρόταση σεντουκιού.

Υλοποίησε τον τύπο Chest, υλοποίησε τη makeChest και διόρθωσε τις υπογραφές τύπων των secureChest και uniqueTreasures. Οι υπογραφές τύπων των makeChest και makeTreasureChest δίνονται ήδη ως μέρος των απαιτήσεων, μην τις τροποποιήσεις.

Ένα Chest πρέπει να περιέχει τα ίδια δεδομένα με ένα TreasureChest και πρέπει να έχει δύο ορίσματα τύπων, ένα για το treasure και έναν phantom τύπο record για το conditions. Σημείωσε ότι επειδή το Chest χρησιμοποιεί έναν phantom type, και επομένως θα πρέπει να είναι αδιαφανές ώστε να εμποδίζεται η χρήση του οπουδήποτε εκτός της μονάδας TreasureFactory. Σε αυτή την περίπτωση, δεν εκτίθεται καθόλου.

Τροποποίησε τις υπογραφές τύπων των secureChest και uniqueTreasures ώστε να προσθέσεις τους περιορισμούς χρησιμοποιώντας επεκτάσιμες εγγραφές ως phantom types. Η secureChest πρέπει να δέχεται ένα Chest χωρίς συγκεκριμένες συνθήκες και να επιστρέφει ένα Maybe Chest, με την επιπλέον συνθήκη securePassword : () στην phantom εγγραφή της. Η uniqueTreasures πρέπει να δέχεται μια List Chest χωρίς συγκεκριμένες συνθήκες και να επιστρέφει μια List Chest, με την επιπλέον συνθήκη uniqueTreasure : () να προστίθεται στην phantom εγγραφή.

Note

Τα tests της Elm έχουν πρόσβαση σε εκτεθειμένες συναρτήσεις, αλλά όχι σε υπογραφές τύπων, επομένως είναι αδύνατο για τα tests να επαληθεύσουν ότι χρησιμοποιείς τις σωστές υπογραφές. Φυσικά, η ισχυρότερη ένδειξη ότι έχεις τις σωστές υπογραφές είναι να καταφέρεις να μεταγλωττίσεις και να τρέξεις τη μονάδα και τα tests, αλλά έχουμε δημιουργήσει έναν κανόνα ανάλυσης που θα ελέγχει τις υπογραφές τύπων σου όταν υποβάλεις τη λύση σου.

2. Διάλεξε τα ασφαλή σεντούκια

Μόλις έχεις έτοιμα τα σεντούκια, πρέπει να ξεχωρίσεις τα ασφαλή.

Υλοποίησε τη secureChest που επιστρέφει παραλλαγή Just μόνο για σεντούκια με κωδικό πρόσβασης 8 ή περισσότερων χαρακτήρων. Τα Chest που πληρούν αυτές τις συνθήκες πρέπει να έχουν μια επιπλέον συνθήκη securePassword : () στον phantom τύπο τους.

3. Διάλεξε μοναδικούς θησαυρούς

Μόνο οι πιο σπάνιοι θησαυροί πρέπει να επιτρέπονται σε ένα μπουντρούμι: ακόμη και ένα μόνο αντίτυπο κάνει έναν θησαυρό να δείχνει φθηνός.

Υλοποίησε τη uniqueTreasures που παίρνει μια λίστα από Chest και επιστρέφει μια λίστα με τα Chest που έχουν μοναδικό θησαυρό στη λίστα εισόδου. Αν ένας θησαυρός εμφανίζεται δύο φορές στην είσοδο, δεν πρέπει να βρίσκεται στην έξοδο. Τα Chest εισόδου δεν πρέπει να έχουν συγκεκριμένες συνθήκες και σε αυτά της εξόδου πρέπει να προστεθεί η επιπλέον συνθήκη uniqueTreasure : ().

4. Απόλαυσε το τελικό προϊόν

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

Υλοποίησε τη makeTreasureChest που παίρνει ένα Chest που είναι ταυτόχρονα ασφαλές και μοναδικό και δημιουργεί ένα TreasureChest. Επειδή το TreasureChest είναι αδιαφανής τύπος, αυτός θα είναι ο μόνος τρόπος να δημιουργήσεις ένα, ακόμα και οι κακοί διευθυντές μπουντρουμιών δε θα μπορούν να τα κάνουν θάλασσα.

Επεξεργασία μέσω GitHub Ο σύνδεσμος ανοίγει σε νέο παράθυρο ή καρτέλα
Elm Exercism

Έτοιμος να ξεκινήσεις την άσκηση Εργοστάσιο θησαυρών;

Γράψου στο Exercism για να μάθεις και να κατακτήσεις Elm με 28 έννοιες110 ασκήσεις και πραγματική καθοδήγηση από ανθρώπους, όλα δωρεάν.