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