Kurzusok
/
Elm
Elm
/
Feladatok
/
Paola rangos pizzája
Paola rangos pizzája

Paola rangos pizzája

Tanulófeladat

Bevezetés

Parszolás

A parszolás a szöveg jelentéssel bíró adattá alakításának folyamata. A String csomag függvényei ebből a szempontból korlátozottak, a reguláris kifejezéseket pedig nehéz használni, ezért Elmében a parszolás idiomatikus módja az elm/parser csomag használata.

Nézzünk meg egy példát arra, hogyan írjunk parsert a LOLCODE programozási nyelv egy részhalmazához. Vegyük a következő megjegyzésekkel ellátott programot:

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

Először írjunk egy Elm-típust a lehetséges parancsok reprezentálására:

type Command
    = Version Int
    | Import String
    | Print String

A LOLCODE-programunk ekkor egy List Command-ként modellezhető. Kezdjük a HAI 1 sor parserével:

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

A versionP egy parser pipeline felhasználásával épül fel. A (|.) operátor jelentése: „parszoláld a szöveget, de dobd el az eredményt”, a (|=) pedig: „parszoláld a szöveget, és tartsd meg az eredményt”. Ebben a példában a Parser.keyword "HAI" felemészt egy „HAI”-t, de ez az eredmény figyelmen kívül marad. A Parser.spaces tetszőleges számú ' ', '\n' és '\r' karaktert felemészt, de az is figyelmen kívül marad. A Parser.int egy egész számot parszol, és ennek az egész számnak az értékét (mondjuk 1) átadja a legfelső Parser.succeed Version-nek, a parser pedig sikeresen visszaadja a Version 1 értéket.

Ha a pipeline bármelyik parsere meghiúsul, egy Parser.DeadEnd-et ad vissza, és az egész parser meghiúsul. Egy parser a Parser.run segítségével futtatható:

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

Vegyük most a „CAN HAS STDIO?” sort ezzel:

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

Az importP szintén egy parser pipeline, de ezúttal a végén a Parser.symbol áll, amely az olyan szimbólumok parszolására alkalmas, mint a „?”. A megtartani kívánt értéket (a beimportálandó csomag nevét) a következő parszolja:

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

Mivel nem tudjuk, mi lesz a csomag neve, nem használhatjuk a Parser.keyword nevű függvényt, helyette a Parser.chompWhile-t használjuk, amely addig emészti egyenként a karaktereket, amíg azok megfelelnek egy feltételnek (itt annak, hogy alfanumerikus karakterek). Ezeket a felemésztett karaktereket azután a Parser.getChompedString gyűjti össze, amely egy Parser String-et ad vissza. Ebben az esetben szeretnénk normalizálni a csomagneveket, ezért a Parser.map segítségével módosítjuk a parserben lévő szöveget, és csupa kisbetűssé tesszük.

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

Következik a VISIBLE "HAI WORLD!", amelyet a következő parszol:

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

Most már minden parancshoz van egy parserünk. Persze egy általános programban nem lehet előre megjósolni a parancsok sorrendjét, ezért készítünk egy általános parancsparsert

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

A Parser.oneOf a megadott sorrendben próbál ki egy parsert. Ha egy parser meghiúsul, a következőt próbálja ki, ha viszont sikerül, az eredményét adja vissza anélkül, hogy kipróbálná a többi parsert. Ha minden parser meghiúsul, a Parser.oneOf is meghiúsul.

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

Figyeld meg, hogy minden sikertelen próbálkozás egy Parser.DeadEnd-et hoz létre, amelyet a Parser.oneOf gyűjt össze.

Egy teljes program parszolásához egymás után több parancsot kell parszolnunk ezzel:

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

A Parser.sequence egy nagyon testreszabható parser, számos beállítási lehetőséggel. A start, separator és end olyan szövegeket vár, amelyek elválasztják a sorozat elemeit; egy Elm-lista parszolásához a „[”, a „,” és a „]” karaktereket használnánk. A spaces rugalmasságot biztosít azáltal, hogy olyan parsert kap, amely kezeli az elemek és az elválasztók közötti esetleges szóközöket; egy Elm-lista parszolásához a Parser.spaces-t használnánk, ebben az esetben viszont egyáltalán nem fogadunk el szóközöket, ezért a Parser.succeed ()-t használjuk, amely azonnal sikeres lesz anélkül, hogy bármit is felemésztene. Az item a sorozat egyes elemeihez tartozó parsert adja meg, a trailing pedig arról dönt, hogy a záró elválasztók Parser.Forbidden, Parser.Mandatory vagy Parser.Optional típusúak-e.

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

Amikor egy parser sikeres, nem fogyaszt több karaktert a szövegből

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

Ezért ha meg akarunk bizonyosodni róla, hogy a szöveg egészét feldolgoztuk, használhatjuk a Parser.end-et

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

Végül néha a parser beolvasott tartalma alapján kell eldöntenünk, hogy meghiúsítsuk-e a parsert. Például ha olyan parserre lenne szükségünk, amely megbuktatja azokat a programokat, amelyek két „HAI” paranccsal kezdődnek egymás után, használhatnánk ezt:

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

                    _ ->
                        Parser.succeed commands
            )

A Parser.andThen megvizsgálhatja egy parser tartalmát, és annak értékétől függően egy új parsert ad vissza. Itt az egyik esetben a Parser.problem segítségével egy saját üzenettel buktattuk meg a parsert, a másik esetben pedig az eredeti értéket adtuk vissza.

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

Utasítások

Izgatottan csatlakozol a Paola's Prestigious Pizza étteremhez új étteremvezetőként. Sok terved van arra, hogyan működtessed hatékonyan a helyet, ehhez pedig adatokra lesz szükséged. Először is elemezned kell az étlapot.

Az étlapon sokféle pizza szerepel; Paola adott neked egy listát egy szöveges fájlban, soronként egy pizzával. Minden pizzabejegyzés három részből áll: a nevéből, egy opcionális vegetáriánus jelölésből és egy árból.

1. A pizzaár értelmezése

Az étlap első néhány sorát látod:

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

Az ár jó kiindulópontnak tűnik. Minden ár egész szám, euróban megadva.

Valósítsd meg a priceParser függvényt az árak értelmezéséhez. Mivel minden ár euróban van, nem kell nyilvántartanod a pénznemet, viszont továbbra is értelmezned kell azt.

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

2. A vegetáriánus jelölés értelmezése

Megint az étlapot nézed:

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

A vegetáriánus jelölés, a "(v)" is meglehetősen egyszerű, úgy döntesz, hogy azt csinálod meg következőnek!

Valósítsd meg a vegetarianParser függvényt a jelölés értelmezéséhez. A parsernek egy Bool értéket kell visszaadnia: True, ha a jelölés ott van, és False, ha nincs.

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

3. A pizza- és hozzávalónevek értelmezése

Rendben, mi következik?

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

A hozzávalók és a pizzanevek meglehetősen egyszerűnek tűnnek: mindegyik egyetlen szó, amelyet kis- és nagybetűs ASCII-karakterek alkotnak.

Valósítsd meg a wordParser függvényt a nevek értelmezéséhez. Miközben ezt csinálod, az egységesség kedvéért alakítsd az összes szót kisbetűssé.

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

4. A hozzávalók listájának értelmezése

A hozzávalók viszont csoportokban fordulnak elő:

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

Ezek egyetlen szavak, amelyeket vesszők és esetleg szóközök választanak el.

Valósítsd meg az ingredientsParser függvényt a hozzávalók értelmezéséhez.

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

5. A teljes pizza értelmezése

Most, hogy megvan az összes hozzávaló, itt az idő, hogy elkészíts egy Pizza-t!

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

Először a pizzanév következik, majd az opcionális vegetáriánus jelölés, egy kettőspont ':', a hozzávalók listája, egy kötőjel '-' és az ár, esetlegesen közbeszúrt szóközökkel.

Valósítsd meg a pizzaParser függvényt a teljes pizza értelmezéséhez.

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

6. A teljes étlap értelmezése

Buli van!

Valósítsd meg a menuParser függvényt, amellyel a szöveges fájlban újsor-karakterekkel '\n' elválasztott összes pizza listáját értelmezheted. Győződj meg róla, hogy az összes pizzát megkapod, úgy hogy a parsert a fájl végéig futtatod.

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. A több szóból álló hozzávalónevek értelmezése

Jaj ne, úgy tűnik, valamit kihagytál. Valahol lejjebb az étlapon néhány hozzávaló nem egyetlen szóból áll:

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

Valósítsd meg a oneIngredientParser függvényt, amely kis- és nagybetűs ASCII-karaktereket vagy szóközöket ' ' fogad el. Használd ki az alkalmat arra is, hogy kiküszöböld a wordParser egy apró hibáját: gondoskodj róla, hogy az üres stringek ne legyenek érvényes hozzávalóként felismerve, hanem helyette Problem "empty string" hibát adjanak ki. Miközben ezt csinálod, gondoskodj róla, hogy a stringek kisbetűssé váljanak, és a szóköz-karaktereket mindkét oldalról vágd le.

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

Miután a parser elkészült, használd az ingredientsParser-ben.

Szerkesztés GitHubon A hivatkozás új ablakban vagy lapon nyílik meg
Elm Exercism

Készen állsz elkezdeni a(z) Paola rangos pizzája feladatot?

Iratkozz fel az Exercism-re, hogy megtanuld és elsajátítsd a(z) Elm nyelvet 28 fogalom110 feladat segítségével, valódi emberi mentorálással, mindez ingyen.