التحليل هو عملية تحويل النص إلى بيانات ذات معنى.
دوال حزمة 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)
وقت الاحتفال!
نفّذ menuParser لتحليل قائمة بجميع البيتزا مفصولة بمحرف سطر جديد '\n' في الملف النصي.
تأكّد من أنك التقطت جميع البيتزا بتشغيل المحلّل حتى يصل إلى نهاية الملف.
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 تمارين، وإرشاد بشري حقيقي، وكل ذلك مجانًا.