ট্র্যাক
/
Elm
Elm
/
অনুশীলনী
/
পাওলার বিখ্যাত পিৎজা
পাওলার বিখ্যাত পিৎজা

পাওলার বিখ্যাত পিৎজা

লার্নিং অনুশীলনী

ভূমিকা

পার্সিং

পার্সিং হলো টেক্সটকে অর্থবহ ডেটায় রূপান্তর করার প্রক্রিয়া। এই ব্যাপারে 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 আপনাকে একটি টেক্সট ফাইলে একটি তালিকা দিয়েছেন, যেখানে প্রতি লাইনে একটি করে পিৎজা আছে। প্রতিটি পিৎজা এন্ট্রিতে তিনটি অংশ থাকে: তার নাম, একটি ঐচ্ছিক নিরামিষ নির্দেশক, আর একটি দাম।

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 ইমপ্লিমেন্ট করুন। পার্সারটির একটি 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. সম্পূর্ণ মেনু পার্স করা

এবার পার্টির সময়!

টেক্সট ফাইলে নিউলাইন ক্যারেক্টার '\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 }]

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

পার্সারটি ডিফাইন করা হয়ে গেলে, এটি ingredientsParser-এ ব্যবহার করুন।

GitHub-এর মাধ্যমে সম্পাদনা করুন লিংকটি একটি নতুন উইন্ডো বা ট্যাবে খোলে
Elm Exercism

পাওলার বিখ্যাত পিৎজা শুরু করতে প্রস্তুত?

Exercism-এ সাইন আপ করুন, Elm ট্র্যাকের 28টি কনসেপ্ট110টি অনুশীলনী আর সত্যিকারের মানুষের মেন্টরিং দিয়ে শিখুন ও দক্ষ হয়ে উঠুন, সম্পূর্ণ বিনামূল্যে।