Διαδρομές
/
Elm
Elm
/
Ασκήσεις
/
Η περίφημη πίτσα της Paola
Η περίφημη πίτσα της Paola

Η περίφημη πίτσα της Paola

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

Εισαγωγή

Συντακτική ανάλυση

Η συντακτική ανάλυση είναι η διαδικασία μετασχηματισμού κειμένου σε δεδομένα με νόημα. Οι συναρτήσεις από το πακέτο String έχουν περιορισμούς ως προς αυτό και οι κανονικές εκφράσεις είναι δύσκολες στη χρήση, οπότε ο ιδιωματικός τρόπος για συντακτική ανάλυση στο Elm είναι με το πακέτο elm/parser.

Ας δούμε ένα παράδειγμα όπου γράφουμε έναν αναλυτή για (ένα υποσύνολο της) γλώσσας προγραμματισμού LOLCODE. Ας δούμε το παρακάτω πρόγραμμα με σχόλια:

HAI 1                 # program version 1
CAN HAS STDIO?        # import stdio
VISIBLE "HAI WORLD!"  # print "HAI WORLD!"
KTHXBYE               # end of program

Πρώτα, ας γράψουμε έναν τύπο Elm που να αναπαριστά τις πιθανές εντολές:

type Command
    = Version Int
    | Import String
    | Print String

Το πρόγραμμά μας σε LOLCODE μπορεί τότε να μοντελοποιηθεί ως List Command. Ας ξεκινήσουμε με έναν αναλυτή για τη γραμμή HAI 1:

versionP : Parser Command
versionP =
    Parser.succeed Version
        |. Parser.keyword "HAI"
        |. Parser.spaces
        |= Parser.int

Ο versionP είναι φτιαγμένος χρησιμοποιώντας ένα pipeline αναλυτή. Ο τελεστής (|.) σημαίνει "ανάλυσε τη συμβολοσειρά αλλά πέταξε το αποτέλεσμα" και ο (|=) σημαίνει "ανάλυσε τη συμβολοσειρά και κράτησε το αποτέλεσμα". Σε αυτό το παράδειγμα, το Parser.keyword "HAI" θα καταναλώσει ένα "HAI", αλλά αυτό το αποτέλεσμα αγνοείται. Το Parser.spaces θα καταναλώσει οποιονδήποτε αριθμό χαρακτήρων ' ', '\n' και '\r', αλλά κι αυτό θα αγνοηθεί. Το Parser.int θα αναλύσει έναν ακέραιο, και η τιμή αυτού του ακεραίου (ας πούμε 1) θα περαστεί στο Parser.succeed Version που βρίσκεται στην κορυφή, και ο αναλυτής θα καταφέρει να επιστρέψει την τιμή Version 1.

Αν αποτύχει οποιοσδήποτε από τους αναλυτές του pipeline, θα επιστρέψει ένα Parser.DeadEnd και ολόκληρος ο αναλυτής θα αποτύχει. Ένας αναλυτής μπορεί να εκτελεστεί με το Parser.run:

Parser.run versionP "HAI 1"
    --> Ok (Version 1)
Parser.run versionP "HAI 2"
    --> Ok (Version 2)
Parser.run versionP "BYE 2"
    --> Err [{ problem = ExpectingKeyword "HAI", col = 1, row = 1 }]
Parser.run versionP "HAI MOM"
    --> Err [{ problem = ExpectingInt, col = 5, row = 1 }]

Ας καταπιαστούμε με τη γραμμή "CAN HAS STDIO?" με

importP : Parser Command
importP =
    Parser.succeed Import
        |. Parser.keyword "CAN HAS"
        |. Parser.spaces
        |= packageP
        |. Parser.symbol "?"

Ο importP είναι επίσης ένα pipeline αναλυτή, αλλά αυτή τη φορά τελειώνει με το Parser.symbol, που είναι κατάλληλο για την ανάλυση συμβόλων όπως το "?". Η τιμή που θέλουμε να κρατήσουμε (το όνομα του πακέτου προς εισαγωγή) αναλύεται από

packageP : Parser String
packageP =
    Parser.chompWhile Char.isAlphaNum
        |> Parser.getChompedString
        |> Parser.map String.toLower

Επειδή δεν ξέρουμε ποιο θα είναι το όνομα του πακέτου, δεν μπορούμε να χρησιμοποιήσουμε το Parser.keyword, οπότε αντ' αυτού χρησιμοποιούμε το Parser.chompWhile, που καταναλώνει χαρακτήρες έναν έναν όσο πληρούν μια συνθήκη (εδώ, το να είναι αλφαριθμητικοί). Αυτοί οι χαρακτήρες που καταναλώθηκαν συλλέγονται έπειτα από το Parser.getChompedString, που επιστρέφει ένα Parser String. Σε αυτή την περίπτωση, θέλουμε να κανονικοποιήσουμε τα ονόματα των πακέτων, οπότε χρησιμοποιούμε το Parser.map για να τροποποιήσουμε τη συμβολοσειρά που περιέχεται στον αναλυτή και να τη μετατρέψουμε σε πεζά.

Parser.run importP "CAN HAS STDIO?"
    --> Ok (Import "stdio")
Parser.run importP "CAN HAS StDiO?"
    --> Ok (Import "stdio")
Parser.run importP "CAN HAS STDIO"
    -->  Err [{ problem = ExpectingSymbol "?", col = 14, row = 1 }]

Στη συνέχεια, το VISIBLE "HAI WORLD!" αναλύεται από

printP : Parser Command
printP =
    Parser.succeed Print
        |. Parser.keyword "VISIBLE"
        |. Parser.spaces
        |. Parser.symbol "\""
        |= stringP
        |. Parser.symbol "\""

stringP : Parser String
stringP =
    Parser.chompWhile (\c -> c /= '"')
        |> Parser.getChompedString

Parser.run printP "VISIBLE \"HAI WORLD!\""
    --> Ok (Print "HAI WORLD!")

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

commandP : Parser Command
commandP =
    Parser.oneOf
        [ versionP
        , importP
        , printP
        ]

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

Parser.run commandP "HAI 2"
    --> Ok (Version 2)
Parser.run commandP "CAN HAS STDIO?"
    --> Ok (Import "stdio")
Parser.run commandP "VISIBLE \"HAI WORLD!\""
    --> Ok (Print "HAI WORLD!")
Parser.run commandP "O NOES"
    --> Err [ { problem = ExpectingKeyword "HAI", col = 1, row = 1 }
    --      , { problem = ExpectingKeyword "CAN HAS", col = 1, row = 1 }
    --      , { problem = ExpectingKeyword "VISIBLE", col = 1, row = 1 }
    --      ]

Πρόσεξε πώς κάθε αποτυχημένη προσπάθεια παράγει ένα Parser.DeadEnd, που συλλέγεται από το Parser.oneOf.

Για να αναλύσουμε ένα ολόκληρο πρόγραμμα, πρέπει να αναλύσουμε πολλές εντολές διαδοχικά με

programP : Parser (List Command)
programP =
    Parser.sequence
        { start = ""
        , separator = "\n"
        , end = "KTHXBYE"
        , spaces = Parser.succeed ()
        , item = commandP
        , trailing = Parser.Optional
        }

Το Parser.sequence είναι ένας πολύ προσαρμόσιμος αναλυτής με αρκετές επιλογές. Τα start, separator και end παίρνουν συμβολοσειρές που χωρίζουν τα στοιχεία της ακολουθίας. Για την ανάλυση μιας λίστας Elm θα χρησιμοποιούσαμε τα "[", "," και "]". Το spaces παρέχει ευελιξία παίρνοντας έναν αναλυτή που θα χειριστεί τα πιθανά κενά ανάμεσα στα στοιχεία και τους διαχωριστές. Για την ανάλυση μιας λίστας Elm θα χρησιμοποιούσαμε το Parser.spaces, αλλά σε αυτή την περίπτωση δεν δεχόμαστε κενά, οπότε χρησιμοποιούμε το Parser.succeed (), που πετυχαίνει αμέσως χωρίς να καταναλώσει στην πραγματικότητα τίποτα. Το item παίρνει τον αναλυτή στοιχείου και, τέλος, το trailing μάς αφήνει να αποφασίσουμε αν οι τελικοί διαχωριστές είναι Parser.Forbidden, Parser.Mandatory ή Parser.Optional.

Parser.run programP "HAI 2\nCAN HAS STDIO?\nKTHXBYE"
    --> Ok [Version 2, Import "stdio"]
Parser.run programP "KTHXBYE"
    --> Ok []
Parser.run programP "HAI 2\nCAN HAS STDIO?\n"
    --> Err [ { problem = ExpectingKeyword "HAI", col = 1, row = 3 }
    --      , { problem = ExpectingKeyword "CAN HAS", col = 1, row = 3 }
    --      , { problem = ExpectingKeyword "VISIBLE", col = 1, row = 3 }
    --      , { problem = Expecting "KTHXBYE", col = 1, row = 3 }
    --      ]

Όταν οι αναλυτές πετυχαίνουν, σταματούν να καταναλώνουν τη συμβολοσειρά

Parser.run Parser.int "1"
    --> Ok 1
Parser.run Parser.int "1 BTW VISIBLE \"U SEE NOTHING\""
    --> Ok 1

Έτσι, αν χρειάζεται να βεβαιωθούμε ότι αναλύσαμε τα πάντα στη συμβολοσειρά, μπορούμε να χρησιμοποιήσουμε το Parser.end

fullProgramP : Parser (List Command)
fullProgramP =
    programP
        |. Parser.spaces
        |. Parser.end

Parser.run fullProgramP "HAI 2\nCAN HAS STDIO?\nKTHXBYE"
    --> Ok [Version 2, Import "stdio"]
Parser.run fullProgramP "HAI 2\nCAN HAS STDIO?\nKTHXBYE\nBTW VISIBLE \"U SEE NOTHING\""
    --> Err [{ problem = ExpectingEnd, col = 1, row = 4 }]

Τέλος, μερικές φορές χρειάζεται να αποφασίσουμε να αποτύχει ένας αναλυτής ανάλογα με το περιεχόμενο που ανέλυσε. Για παράδειγμα, αν χρειαζόμασταν έναν αναλυτή που να αποτυγχάνει σε προγράμματα που ξεκινούν με δύο εντολές "HAI" στη σειρά, θα μπορούσαμε να χρησιμοποιήσουμε

singleVersionProgramP : Parser (List Command)
singleVersionProgramP =
    fullProgramP
        |> Parser.andThen
            (\commands ->
                case commands of
                    (Version _) :: (Version _) :: _ ->
                        Parser.problem "Multiple versions defined"

                    _ ->
                        Parser.succeed commands
            )

Το Parser.andThen μπορεί να εξετάσει το περιεχόμενο ενός αναλυτή και επιστρέφει έναν νέο αναλυτή ανάλογα με την τιμή του. Εδώ, στη μία περίπτωση κάναμε τον αναλυτή να αποτύχει με ένα προσαρμοσμένο μήνυμα χρησιμοποιώντας το Parser.problem, και στην άλλη επιστρέψαμε την αρχική τιμή.

Parser.run singleVersionProgramP "HAI 2\nCAN HAS STDIO?\nKTHXBYE"
    --> Ok [Version 2, Import "stdio"]
Parser.run singleVersionProgramP "HAI 1\nHAI 2\nCAN HAS STDIO?\nKTHXBYE"
    --> Err [{ problem = Problem "Multiple versions defined", col = 8, row = 4 }]

Οδηγίες

Είσαι ενθουσιασμένος που αναλαμβάνεις τη θέση του νέου διευθυντή στο εστιατόριο Paola's Prestigious Pizza. Έχεις πολλά σχέδια για το πώς θα λειτουργήσεις το μαγαζί αποδοτικά, και για να το κάνεις αυτό, χρειάζεσαι κάποια δεδομένα. Πρώτα από όλα, πρέπει να αναλύσεις το μενού.

Υπάρχουν πολλά είδη πίτσας στο μενού. Η Paola σου έδωσε μια λίστα σε ένα αρχείο κειμένου, με μία πίτσα σε κάθε γραμμή. Κάθε καταχώριση πίτσας έχει τρία στοιχεία: το όνομά της, έναν προαιρετικό δείκτη χορτοφαγίας και μια τιμή.

1. Ανάλυσε την τιμή της πίτσας

Κοιτάς τις πρώτες δύο γραμμές του μενού:

Regina: tomato, ham, mushrooms, cantal - 11€
Formaggio (v): tomato, emmental - 8€
...

Η τιμή φαίνεται σαν καλή αρχή. Όλες οι τιμές είναι ακέραιοι αριθμοί σε ευρώ.

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

Parser.run priceParser "8€"
    --> Ok 8
Parser.run priceParser "8"
    --> Err [{ problem = ExpectingSymbol "€", col = 2, row = 1 }]

2. Ανάλυσε τον δείκτη χορτοφαγίας

Κοιτάς ξανά το μενού:

Regina: tomato, ham, mushrooms, cantal - 11€
Formaggio (v): tomato, emmental - 8€
...

Ο δείκτης χορτοφαγίας "(v)" είναι αρκετά απλός, αποφασίζεις να κάνεις αυτό μετά!

Υλοποίησε τον vegetarianParser για να αναλύσεις τον δείκτη. Ο parser θα πρέπει να επιστρέφει μια Bool, True αν ο δείκτης υπάρχει και False διαφορετικά.

Parser.run vegetarianParser "(v)"
    --> Ok True
Parser.run vegetarianParser ""
    --> Ok False

3. Ανάλυσε τα ονόματα πίτσας και συστατικών

Εντάξει, ποιο είναι το επόμενο;

Regina: tomato, ham, mushrooms, cantal - 11€
Formaggio (v): tomato, emmental - 8€
...

Τα συστατικά και τα ονόματα των πιτσών φαίνονται αρκετά απλά: είναι όλα μεμονωμένες λέξεις φτιαγμένες από κεφαλαίους και πεζούς χαρακτήρες ASCII.

Υλοποίησε τον wordParser για να αναλύσεις τα ονόματα. Με την ευκαιρία, κάνε όλες τις λέξεις πεζές για ομοιομορφία.

Parser.run wordParser "REGINA"
    --> Ok "regina"
Parser.run wordParser "tomato"
    --> Ok "tomato"
Parser.run wordParser "(v)"
    --> Ok ""
Parser.run wordParser ""
    --> Ok ""

4. Ανάλυσε μια λίστα συστατικών

Τα συστατικά, ωστόσο, έρχονται σε ομάδες:

Regina: tomato, ham, mushrooms, cantal - 11€
Formaggio (v): tomato, emmental - 8€
...

Είναι μεμονωμένες λέξεις χωρισμένες με κόμματα και πιθανώς κενά.

Υλοποίησε τον ingredientsParser για να αναλύσεις τα συστατικά.

Parser.run ingredientsParser "tomato, emmental"
    --> Ok ["tomato", "emmental"]
Parser.run ingredientsParser "tomato"
    --> Ok ["tomato"]

5. Ανάλυσε μια ολόκληρη πίτσα

Τώρα που έχεις όλα τα συστατικά, ήρθε η ώρα να φτιάξεις μια Pizza!

Regina: tomato, ham, mushrooms, cantal - 11€
Formaggio (v): tomato, emmental - 8€
...

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

Υλοποίησε τον pizzaParser για να αναλύσεις την ολόκληρη πίτσα.

Parser.run pizzaParser "Regina: tomato, ham, mushrooms, cantal - 11€"
    --> Ok (Pizza "regina" False ["tomato", "ham", "mushrooms", "cantal"] 11)

6. Ανάλυσε το πλήρες μενού

Ώρα για γλέντι!

Υλοποίησε τον menuParser για να αναλύσεις μια λίστα με όλες τις πίτσες που χωρίζονται με χαρακτήρες αλλαγής γραμμής '\n' στο αρχείο κειμένου. Βεβαιώσου ότι πήρες όλες τις πίτσες τρέχοντας τον parser μέχρι να φτάσει στο τέλος του αρχείου.

Parser.run menuParser "Regina: tomato, ham, mushrooms, cantal - 11€\nFormaggio (v): tomato, emmental - 8€"
    --> Ok [Pizza "regina" False ["tomato", "ham", "mushrooms", "cantal"] 11,
    --      Pizza "formaggio" True ["tomato", "emmental"] 8]
Parser.run menuParser "Regina: tomato, ham, mushrooms, cantal - 11€\[END]"
    --> Err [{ problem = ExpectingEnd, col = 1, row = 2 }]

7. Ανάλυσε ονόματα συστατικών με πολλές λέξεις

Ωχ όχι, φαίνεται ότι σου ξέφυγε κάτι. Κάπου πιο κάτω στο μενού, μερικά συστατικά δεν είναι μεμονωμένες λέξεις:

...
Tonno: tomato sauce, tuna - 10€
Hawaii: tomato sauce, fresh pineapple, ham - 9€
...

Υλοποίησε τον oneIngredientParser που θα δέχεται κεφαλαίους και πεζούς χαρακτήρες ASCII ή κενά ' '. Επίσης, εκμεταλλεύσου την ευκαιρία για να αποφύγεις ένα μικρό ελάττωμα που είχε ο wordParser, βεβαιώνοντας ότι οι κενές συμβολοσειρές δεν αναγνωρίζονται ως έγκυρα συστατικά και αντ' αυτού εκπέμπουν Problem "empty string". Με την ευκαιρία, βεβαιώσου ότι κάνεις τις συμβολοσειρές πεζές και επίσης αφαιρείς τους χαρακτήρες κενού από τις δύο πλευρές.

Parser.run oneIngredientParser "Tomato Sauce"
    --> Ok "tomato sauce"
Parser.run oneIngredientParser "   tomato sauce     "
    --> Ok "tomato sauce"
Parser.run oneIngredientParser ""
    --> Err [{ problem = Problem "empty string", col = 1, row = 1 }]

Μόλις οριστεί ο parser, χρησιμοποίησέ τον στον ingredientsParser.

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

Έτοιμος να ξεκινήσεις την άσκηση Η περίφημη πίτσα της Paola;

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