पार्सिंग वह प्रक्रिया है जिसमें टेक्स्ट को अर्थपूर्ण डेटा में बदला जाता है।
इस मामले में 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 को एक पार्सर पाइपलाइन की मदद से बनाया गया है।
(|.) ऑपरेटर का मतलब है "स्ट्रिंग को पार्स कीजिए, पर नतीजा छोड़ दीजिए" और (|=) का मतलब है "स्ट्रिंग को पार्स कीजिए और नतीजा रख लीजिए"।
इस उदाहरण में, Parser.keyword "HAI" एक "HAI" पढ़ेगा, पर उस नतीजे को अनदेखा कर दिया जाता है।
Parser.spaces कितने भी ' ', '\n', और '\r' अक्षर पढ़ लेगा, पर उसे भी अनदेखा कर दिया जाएगा।
Parser.int एक पूर्णांक पार्स करेगा, और उस पूर्णांक की वैल्यू (मान लीजिए 1) सबसे ऊपर वाले Parser.succeed Version को दी जाएगी, और पार्सर Version 1 वैल्यू लौटाने में सफल होगा।
अगर पाइपलाइन का कोई भी पार्सर फेल हो जाए, तो वह एक 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 भी एक पार्सर पाइपलाइन है, पर इस बार यह 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 ने आपको एक टेक्स्ट फाइल में एक सूची दी है, जिसमें हर लाइन पर एक पिज़्ज़ा है। सूची में हर पिज़्ज़ा की प्रविष्टि के तीन हिस्से होते हैं: उसका नाम, एक वैकल्पिक शाकाहारी संकेत, और कीमत।
आप मेन्यू की पहली कुछ लाइनें देखते हैं:
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 }]
आप फिर से मेन्यू देखते हैं:
Regina: tomato, ham, mushrooms, cantal - 11€
Formaggio (v): tomato, emmental - 8€
...
शाकाहारी संकेत "(v)" भी काफी आसान है, इसलिए आप तय करते हैं कि अगला काम यही होगा!
संकेत को पार्स करने के लिए vegetarianParser बनाइए।
पार्सर को एक Bool लौटाना चाहिए: अगर संकेत मौजूद हो तो True, वरना False.
Parser.run vegetarianParser "(v)"
--> Ok True
Parser.run vegetarianParser ""
--> Ok False
ठीक है, अब आगे क्या?
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 ""
लेकिन सामग्री समूह में आती है:
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"]
अब जब आपके पास सारी सामग्री है, तो कुछ 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)
पार्टी का समय!
टेक्स्ट फाइल में न्यूलाइन अक्षर '\n' से अलग किए गए सभी पिज़्ज़ा की सूची पार्स करने के लिए menuParser बनाइए।
ध्यान रखिए कि आपको सारे पिज़्ज़ा मिल जाएँ, इसके लिए पार्सर को तब तक चलाइए जब तक वह फाइल के अंत तक न पहुँच जाए।
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 }]
अरे नहीं, लगता है आपसे कुछ छूट गया है। मेन्यू में कहीं आगे कुछ सामग्री एक शब्द की नहीं है:
...
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 }]
पार्सर बन जाने के बाद उसे ingredientsParser में इस्तेमाल कीजिए।
Exercism पर साइन अप कीजिए और Elm को 28 कॉन्सेप्ट110 अभ्यास तथा असली इंसानों से मिलने वाली मेंटरिंग के साथ सीखिए और उसमें महारत हासिल कीजिए, वह भी बिल्कुल मुफ्त।