موزعي هاسكل التطبيقيين


الدافع


عندما بدأت في تعلم Haskell لأول مرة ، شعرت بالانزعاج الشديد من الاستخدام الواسع النطاق للتجارب المعقدة بدلاً من أي حلول محددة. يبدو لي أنه من الأفضل دائمًا اتباع مبدأ KISS وكتابة الدراجات باستخدام تصميمات اللغة الأساسية بدلاً من فهم كل فئات الأنواع هذه من أجل كتابة بنية مناسبة من المفترض في مكان ما.


لم يكن لدي مثال جيد حيث ستؤتي الجهود المبذولة في تطوير "العتاد" ثمارها. بالنسبة لي ، كان أحد أكثر الأمثلة نجاحًا هو المحللون. الآن أتحدث عنهم كثيرًا عندما يسألونني عن المهام الشائعة التي يمكنك استخدامها بشكل جميل Haskell.


أريد أن أقدم للمبتدئين أيضًا بهذه الطريقة وإنشاء قاعدة صغيرة من الوظائف من البداية للتنفيذ المريح للمحللات ، ثم استخدمها لكتابة المحلل اللغوي الخاص بهم ، والذي سيكرر رمزه حرفيًا القواعد اللغوية المستخدمة في التحليل.


آمل أن يساعد ذلك أي شخص على التغلب على الخوف من التجريدات وتعليمهم كيفية استخدامه بشكل مناسب (نعم ، ما زلت أعتقد أنه من الأفضل في بعض الأحيان كتابة دراجة هوائية).


ليس لدي أي غرض أو رغبة في جعل دورة Haskell من البداية من مقال ، لذلك افترض أن القارئ معتاد على بناء الجملة والبرامج البسيطة المطورة بشكل مستقل. فقط في حالة ، سأتحدث باختصار عن فئات الكتابة قبل الانتقال إلى وصف التنفيذ.


بالنسبة لأولئك الذين لم يكتبوا إلى Haskell أبدًا ، لكنهم يريدون أن يفهموا ما يحدث هنا ، فإنني أوصي بأن تنظر أولاً إلى الصفحة ذات الصلة على Learn X خلال Y. ككتاب ممتاز باللغة الروسية للمبتدئين ، أنصح بـ "حول هاسكل ككائن بشري" للمخرج دينيس شيفتشينكو


سأحاول استخدام أبسط اللغات التي يمكن للمبتدئين فهمها. في نهاية المقالة ، يتم تقديم رابط إلى مستودع المصدر ، حيث يتم استخدام إدخال أكثر ملاءمة وأقصر في بعض أجزاء الكود ، وهو ما قد يكون أقل وضوحًا من النظرة الأولى.


ونعم ، أيها السادة Haskellists ، يتم شرح العديد من الأشياء بكل بساطة وبشكل أخرق ، في حالات خاصة ، وليس بشكل مجردة للغاية ، دون استخدام مصطلحات من نظرية الفئة وغيرها من الكلمات المخيفة. أنا سعيد لأنك تعرفهم وبالطبع أتقنهم بسهولة. أنا أعرفهم أيضًا ، لكنني لا أعتقد أنه من الضروري إلقاء مثل هذا الكم من المعلومات في هذا السياق على القراء غير المستعدين.


اكتب الطبقات


فئات أنواع Haskell لا علاقة لها بالفئات في لغة C ++ ولغات الكائنات الأخرى الموجهة. إذا رسمنا تشابهاً مع OOP ، فإن الفصول الدراسية تشبه إلى حد كبير الحمل الزائد للأساليب والوظائف.


تحدد الفصول الإجراءات التي يمكن تنفيذها مع كائنات من الأنواع التي تشكل الفصل. على سبيل المثال ، يمكن مقارنة جميع الأرقام من أجل المساواة ، ولكن يمكن ترتيب كل شيء باستثناء الأرقام المعقدة ، وبشكل عام ، لا يمكن مقارنة الوظائف على الإطلاق. فئة الأنواع التي يمكن مقارنتها تسمى Eq ، مرتبة - Ord (لا يجب أن تكون الأنواع رقمية). ما يمكن طباعته بالترجمة إلى سلسلة ينتمي إلى فئة العرض ، ولديه فئة Read "المعاكسة" ، التي تحدد كيفية تحويل السلاسل إلى كائنات من النوع المطلوب.


بالنسبة لمجموعة من فئات Eq القياسية (مثل Eq و Show و Read ...) ، يمكنك مطالبة برنامج التحويل البرمجي بتنفيذ الوظيفة المطلوبة بطريقة قياسية ، باستخدام الكلمة الأساسية deriving بعد تحديد النوع:


 data Point = Point { xCoord :: Float , yCoord :: Float } deriving (Eq, Show) 

يمكنك تحديد فئات الكتابة الخاصة بك:


 class PrettyPrint a where pPrint :: a -> String 

هنا PrettyPrint هو اسم الفئة ، a متغير نوع. الكلمة الأساسية where يتبعها قائمة بأساليب ما يسمى بالفصل الدراسي ، أي الوظائف التي يمكن تطبيقها على كائنات الكتابة من هذه الفئة.


للإشارة إلى الانتماء لنوع البيانات إلى فئة ، يتم استخدام البناء التالي:


 instance PrettyPrint Point where pPrint (Point xy) = "(" ++ show x ++ ", " ++ show y ++ ")" 

تتيح لك اللغة تحديد قيود على فئات الأنواع التي يجب أن ترتبط بها وسيطات الوظيفة:


 showVsPretty :: (Show a, PrettyPrint a) => a -> (String, String) showVsPretty x = (show x, pPrint x) 

لكل استدعاء دالة ، يقوم المحول البرمجي بالتحقق مما إذا كانت متطلبات النوع هذه قد تم الوفاء بها ، وفي حالة الفشل ، يعرض خطأ (بالطبع ، يحدث هذا في مرحلة الترجمة).


 >>> showVsPretty (Point 2 3) ("Point {xCoord = 2.0, yCoord = 3.0}","(2.0, 3.0)") >>> showVsPretty "str" error: No instance for (PrettyPrint [Char]) arising from a use of 'showVsPretty' 

التنفيذ


يتلقى المحلل اللغوي سلسلة إدخال يجب تحليلها وفقًا للقواعد المحددة مسبقًا والحصول على قيمة النوع الذي نحتاجه (على سبيل المثال ، عدد صحيح). في هذه الحالة ، قد لا ينتهي سطر الإدخال ، وسيكون الباقي بمثابة مدخل لمزيد من التحليل. بالإضافة إلى ذلك ، سيكون المحلل اللغوي الخاص بنا غير محدد بشكل عام ، أي سيعود العديد من نتائج تحليل المحتملة كقائمة.


مجموعة مكونة من عنصرين (String, a) مناسبة لوصف نتيجة واحدة لعملية المحلل اللغوي ، حيث يمثل a متغير نوع يمكنه الإشارة إلى أي نوع مستخدم.


نظرًا لأن المحلل اللغوي يوزع السلسلة وفقًا لبعض القواعد ، فإننا نصفها كدالة تأخذ سلسلة كمدخلات وتُرجع قائمة النتائج:


 newtype Parser a = Parser { unParser :: String -> [(String, a)] } 

سننظر في نجاح التحليل إذا كانت قائمة النتائج تتكون من عنصر واحد وتمت معالجة سلسلة الإدخال بالكامل. نقوم بتنفيذ وظيفة المساعد التي تحاول تحليل السلسلة بالكامل بشكل فريد:


 parseString :: String -> Parser a -> Maybe a parseString s (Parser p) = case (ps) of [("", val)] -> Just val _ -> Nothing 

المعرب اللغوي البسيط


نحن نطبق العديد من المحلل اللغوي البسيط ، والذي سيكون مفيدًا في بناء مجموعات أكثر تعقيدًا.


نبدأ من خلال تحليل شخصية واحدة يجب أن تلبي المسند. إذا كانت سلسلة الإدخال فارغة ، فإن نتيجة العمل هي قائمة فارغة. خلاف ذلك ، نتحقق من قيمة المسند في أول حرف من السلسلة. إذا True إرجاع True ، تكون نتيجة التحليل هي هذه الشخصية ؛ إعادته مع بقية السلسلة. خلاف ذلك ، فشل التحليل أيضا.


 predP :: (Char -> Bool) -> Parser Char predP p = Parser f where f "" = [] f (c : cs) | pc = [(cs, c)] | otherwise = [] 

الآن يمكننا كتابة المحلل اللغوي الذي يأخذ شخصية معينة في بداية السطر. للقيام بذلك ، استخدم predP المكتوبة للتو ، predP كوسيطة ، وهي دالة تقارن predP بالحرف الذي نحتاجه:


 charP :: Char -> Parser Char charP char = predP (\c -> c == char) 

الحالة الأبسط التالية: محلل يقبل سلسلة معينة فقط ككل. stringP نسميها stringP . تقارن الوظيفة الموجودة في المحلل اللغوي سطر الإدخال بالخط المرغوب ، وإذا كانت الخطوط متساوية ، فتُرجع قائمة بعنصر واحد: زوج من الخطوط الفارغة (لا يتبقى شيء في الإدخال) والخط الأصلي. خلاف ذلك ، فشل التحليل ، ويتم إرجاع قائمة فارغة من النتائج.


 stringP :: String -> Parser String stringP s = Parser f where fs' | s == s' = [("", s)] | otherwise = [] 

غالبًا ما تحتاج إلى تخطي الأحرف التي لها خاصية معينة أثناء انتقالها إلى بداية السطر (على سبيل المثال ، أحرف المسافات البيضاء). علاوة على ذلك ، فإن نتيجة التحليل ليست مهمة بالنسبة لنا ولن تكون مفيدة في المستقبل. نكتب وظيفة skip تتخطى الأحرف الأولية للسلسلة بينما يتم الحفاظ على القيمة الحقيقية للمسند. نتيجة التحليل ، نستخدم tuple فارغة.


 skip :: (Char -> Bool) -> Parser () skip p = Parser (\s -> [(dropWhile ps, ())]) 

المحللان التاليان متشابهان للغاية مع بعضهما البعض. يتحقق كلاهما من بادئة سطر الإدخال ، فقط الأولى إذا نجحت في إرجاع هذه البادئة ، والثانية تُرجع علامة فارغة ، أي يسمح لك بتخطي خط تعسفي في بداية الإدخال. يستخدم isPrefixOf الدالة isPrefixOf المعرفة في الوحدة النمطية Data.List .


 prefixP :: String -> Parser String prefixP s = Parser f where f input = if s `isPrefixOf` input then [(drop (length s) input, s)] else [] skipString :: String -> Parser () skipString s = Parser f where f input = if s `isPrefixOf` input then [(drop (length s) input, ())] else [] 

بعد قليل سننظر في تطبيق أكثر بساطة لهذه الوظيفة الأخيرة والتخلص من تكرار الكود.


المحلل اللغوي باعتباره functor


يمكننا التمييز بين فئة كاملة من أنواع الحاويات التي ينطبق عليها ما يلي: إذا كنت تعرف كيفية تحويل كائنات داخل حاوية ، فيمكنك حينئذٍ تحويل الحاويات بأنفسها. أبسط مثال هو قائمة كحاوية map وظيفة ، والتي تتوفر في جميع اللغات الرفيعة المستوى تقريبا. في الواقع ، يمكنك استعراض جميع عناصر قائمة النوع [a] ، وتطبيق a -> b على كل منها ، والحصول على قائمة من النوع [b] .


يُطلق على فئة الكتابة هذه اسم Functor ؛ للفئة طريقة fmap واحدة:


 class Functor f where fmap :: (a -> b) -> fa -> fb 

افترض أننا نعرف بالفعل كيفية تحليل السلاسل إلى كائنات من نوع معين ، بالإضافة إلى ذلك ، نحن نعرف كيفية تحويل الكائنات من النوع a إلى كائنات من النوع b . هل من الممكن القول أنه يوجد محلل للكائنات من النوع b ؟


إذا تم التعبير عنها في شكل دالة ، فسيكون لها النوع التالي:


 (a -> b) -> Parser a -> Parser b 

يتزامن هذا النوع مع نوع وظيفة fmap ، لذلك دعونا نحاول أن نجعل المحلل اللغوي عبارة عن عامل توجيه. لنقم بإنشاء محلل لقيم النوع b من نقطة الصفر ، والتي سوف تستدعي أولاً المحلل اللغوي الأول (لدينا بالفعل واحدة) ، ثم نطبق الوظيفة على نتائج التحليل.


 instance Functor Parser where fmap :: (a -> b) -> Parser a -> Parser b fmap f (Parser p1) = Parser p2 where p2 :: String -> [(String, b)] p2 s = convert (p1 s) convert :: [(String, a)] -> [(String, b)] convert results = map (\(s, val) -> (s, f val)) results 

وظيفة fmap لها مرادف infix مناسب: fmap fx == f <$> x .


إذا استخدمنا إحدى الدالات كوسيطة fmap التي تحل ببساطة محل الوسيطة الأولى لها بقيمة جديدة ، fmap على عملية أخرى مفيدة تم تنفيذها بالفعل لجميع المشغلات حتى في حالتين (تختلف فقط في ترتيب الوسائط):


 (<$) :: Functor f => a -> fb -> fa ($>) :: Functor f => fa -> b -> fb 

تذكر المحلل اللغوي الذي يتخطى خط معين ( skipString )؟ الآن يمكنك تنفيذه على النحو التالي:


 skipString :: String -> Parser () skipString s = () <$ prefixP s 

مجموعات محلل


في Haskell ، يتم إيقاف جميع الوظائف افتراضيًا ويمكن استخدامها جزئيًا. هذا يعني أن دالة الوسيطات n هي في الواقع دالة للوسيطة الواحدة ، حيث تقوم بإرجاع دالة n-1 :


 cons :: Int -> [Int] -> [Int] cons = (:) cons1 :: [Int] -> [Int] cons1 = cons 1 --  cons   

نحن نطبق دالة من ثلاث وسيطات على بعض القيمة داخل المحلل باستخدام fmap . الأنواع ستكون كما يلي:


 f :: c -> a -> b p :: Parser c (fmap fp) :: Parser (a -> b) 

تحولت وظيفة محلل؟! بالطبع ، يكون الموقف ممكنًا عندما يكون تمثيل الوظيفة موجودًا بالفعل في سطر الإدخال ، لكنني أرغب في أن أكون قادرًا على استخدام هذه الوظيفة ، أو بالأحرى الجمع بين Parser (a -> b) والمحلل Parser b للحصول على Parser b :


 applyP :: Parser (a -> b) -> Parser a -> Parser b 

نوع هذه الوظيفة يشبه إلى حد fmap نوع fmap ، فقط الوظيفة نفسها التي يجب تطبيقها في الحاوية. هذا يعطي فهمًا بديهيًا لما يجب أن يبدو عليه تنفيذ وظيفة applyP : احصل على الوظيفة من الحاوية (نتيجة لتطبيق المحلل اللغوي الأول) ، واحصل على القيم التي يجب أن تنطبق عليها الوظيفة (نتيجة لتطبيق المحلل اللغوي الثاني) ، و "حزمة" القيم المحولة باستخدام هذه الوظيفة مرة أخرى في الحاوية (إنشاء محلل جديد). في التنفيذ ، سوف نستخدم فهم القائمة:


 applyP :: Parser (a -> b) -> Parser a -> Parser b applyP (Parser p1) (Parser p2) = Parser f where fs = [ (sx, fx) | (sf, f) <- p1 s, -- p1     (sx, x) <- p2 sf] -- p2   ,     

هناك فئة Applicative التي لديها طريقة مع نفس النموذج. تسمى الطريقة الثانية للفئة pure وتستخدم "للالتفاف" أو "الرفع" ( الرفع ) قيمة ، بما في ذلك القيمة الوظيفية. في حالة تطبيق المحلل اللغوي ، تضيف الدالة pure الوسيطة الخاصة بها إلى نتيجة المحلل اللغوي ، دون تغيير سلسلة الإدخال.


 class Functor f => Applicative f where pure :: a -> fa (<*>) :: f (a -> b) -> fa -> fb instance Applicative Parser where pure x = Parser (\s -> [(s, x)]) pf <*> px = Parser (\s -> [ (sx, fx) | (sf, f) <- unParser pf $ s, (sx, x) <- unParser px $ sf]) 

إن وظيفة applyP هي <*> من الفئة Applicative . تسمى الأنواع التي تنتمي إلى هذه الفئة الدواوين التطبيقية.


بالنسبة للجهات المشغلة للتطبيق ، يتم تنفيذ وظيفتين مساعدتين ستكون مفيدة لنا:


 (*>) :: fa -> fb -> fb (<*) :: fa -> fb -> fa 

هذه الوظائف تؤدي إجراءين متتاليين وتعيد نتيجة واحدة منها فقط. بالنسبة للمحللات ، يمكن استخدامها ، على سبيل المثال ، لتخطي المسافات البادئة قبل تحليل جزء من سلسلة تحمل حملًا دلاليًا.


من خلال الجمع بين <$> و <*> ، يمكنك إنشاء تصميمات مريحة للغاية. النظر في نوع البيانات التالية:


 data MyStructType = MyStruct { field1 :: Type1 , field2 :: Type2 , field3 :: Type3 } 

مُنشئ القيم MyStruct هو أيضًا وظيفة ، وفي هذه الحالة يكون من النوع 1 Type1 -> Type2 -> Type3 -> MyStructType . يمكنك العمل مع المنشئ مثل أي وظيفة أخرى. لنفترض أن المحللون قد كتبوا بالفعل لأنواع حقول البنية:


 parser1 :: Parser Type1 parser2 :: Parser Type2 parser3 :: Parser Type3 

باستخدام وظيفة fmap ، يمكنك تطبيق MyStruct جزئيًا على أول fmap هذه:


 parserStruct' :: Parser (Type2 -> Type3 -> MyStructType) parserStruct' = MyStruct <$> parser1 

دعنا نحاول تطبيق الوظيفة التي هي الآن "بداخل" المحلل اللغوي. للقيام بذلك ، تحتاج بالفعل إلى استخدام <*> :


 parserStruct'' :: Parser (Type3 -> MyStructType) parserStruct'' = parserStruct' <*> parser2 parserStruct :: Parser MyStructType parserStruct = parserStruct'' <*> parser3 

نتيجة لذلك ، حصلنا على محلل للبنية بأكملها (بالطبع ، نستخدم هنا الافتراض بأن تمثيل الحقول في السطر الأصلي في صف واحد). نفس الشيء يمكن القيام به في سطر واحد:


 parserStruct :: Parser MyStructType parserStruct = MyStruct <$> parser1 <*> parser2 <*> parser3 

غالبًا ما تواجه هذه الإنشاءات في حالات الاستخدام.


لنفترض الآن أننا نحاول كتابة محلل يوزع التعبيرات الحسابية البسيطة التي يمكن أن تكون فيها الأعداد الصحيحة والمعرّفات موجودة كصفقات للمعاملات. لنقم بإنشاء نوع منفصل من Operand لهم:


 data Operand = IntOp Int | IdentOp String 

إذا علمنا بالفعل كيفية تحليل الأعداد الصحيحة والمعرّفات (على سبيل المثال ، كما هو الحال في C) ، فإننا نحتاج إلى محلل واحد للمعاملات التي يمكنها تحليل واحد أو الآخر. هذا المحلل هو بديل عن الآخر ، لذلك نحن بحاجة إلى وظيفة يمكن أن تجمع بين المحللون بحيث يتم دمج نتائج عملهم. نتيجة المحلل اللغوي هي قائمة ، والجمع بين القوائم هو تسلسلها. نحن altP وظيفة altP تجمع بين اثنين من altP :


 altP :: Parser a -> Parser a -> Parser a altP (Parser p1) (Parser p2) = Parser (\s -> p1 s ++ p2 s) 

ثم يمكن تنفيذ محلل المعامل باستخدام هذه الوظيفة (هنا يفترض أن parserInt و parserIdent بالفعل في مكان ما:


 parserOperand :: Parser Operand parserOperand = altP parserIntOp parserIdentOp where parserIntOp = IntOp <$> parserInt parserIdentOp = IdentOp <$> parserIdent 

بالطبع ، بالنسبة للبدائل ، توصلنا بالفعل إلى فصل منفصل ، والذي يسمى Alternative . لديها طريقة أخرى ، empty ، تصف العنصر المحايد للعملية البديلة. في حالتنا ، إنه محلل لا يقوم مطلقًا بتوزيع أي شيء ، أي دائما إرجاع قائمة فارغة من النتائج. بالنسبة للمحلل اللغوي ، يبدو تنفيذ أساليب الفئة Alternative كما يلي:


 class Applicative f => Alternative f where empty :: fa (<|>) :: fa -> fa -> fa instance Alternative Parser where empty = Parser (const []) px <|> py = Parser (\s -> unParser px s ++ unParser py s) 

العملية <|> هي وظيفة altP فقط في تدوينات infix ، وهو أكثر ملاءمة للاستخدام من خلال الجمع بين عدة موزعات في صف واحد.


بالنسبة لجميع الأنواع في هذه الفئة ، يتم تنفيذ وظيفتين ، some many الأنواع fa -> f [a] . يمكن التعبير عن كل منهم من خلال الآخر:


 some v = (:) <$> v <*> many v many v = some v <|> pure [] 

بالنسبة للمحللات ، تتيح لك هذه الوظائف تحليل تسلسل البيانات إذا كنت تعرف كيفية تحليل عنصر بيانات واحد. في حالة استخدام some يجب أن يكون التسلسل غير فارغ.


مثال للاستخدام


نحن الآن على استعداد لكتابة المحلل اللغوي الخاص بك ، على سبيل المثال ، للتعبيرات الحسابية البسيطة باستخدام القواعد التالية:


  expr ::= constExpr | binOpExpr | negExpr const ::= int int ::= digit{digit} digit ::= '0' | ... | '9' binOpExpr ::= '(' expr ' ' binOp ' ' expr ')' binOp ::= '+' | '*' negExpr ::= '-' expr 

يتكون التعبير من عدد صحيح من الثوابت ، ناقص أحادي ، واثنين من العمليات الثنائية infix: الجمع والضرب. الأقواس مطلوبة حول تعبير من خلال عملية ثنائية ، يتم فصل رمز العملية عن المعاملات بمسافة واحدة تمامًا ، ولا يُسمح بالمسافات البادئة والمعلقة.


أمثلة لكتابة التعبير الصحيحة:


 "123" "-(10 + 42)" "(1 + ((2 + 3) * (4 + 5)))" 

أمثلة على الإدخالات غير الصحيحة:


 " 666 " "2 + 3" "(10 * 10)" 

نعلن أنواع البيانات اللازمة (التعبير نفسه والعملية الثنائية):


 data Expr = ConstExpr Int | BinaryExpr Expr Operator Expr | NegateExpr Expr data Operator = Add | Mul 

يمكنك البدء في تحليل! يتكون التعبير نفسه من ثلاثة بدائل. لذلك نكتب:


 -- expr ::= constExpr | binOpExpr | negExpr exprParser :: Parser Expr exprParser = constParser <|> binParser <|> negParser 

الثابت هو عدد صحيح موجب. في نوع البيانات الخاص بنا ، يتم "لفه" في المُنشئ ، لذلك لا يمكننا استخدام المحلل اللغوي لعدد صحيح مباشرة ، ولكن يمكننا استخدام fmap للحصول على قيمة النوع المطلوب.


 -- const ::= int constParser :: Parser Expr constParser = ConstExpr <$> intParser 

يتم تمثيل عدد صحيح ، وفقًا لقواعد اللغة ، كسلسلة غير فارغة من الأرقام. لتحليل رقم واحد ، نستخدم الدالة predP والمسند isDigit من وحدة Data.Char . الآن ، لبناء محلل لتحليل تسلسل من الأرقام ، نستخدم الدالة some (ليس many ، لأنه يجب أن يكون هناك رقم واحد على الأقل). تقوم نتيجة المحلل اللغوي بإرجاع قائمة بجميع خيارات التحليل الممكنة ، بدءًا من أطول سجل. على سبيل المثال ، إذا كانت سلسلة الإدخال هي "123ab" ، فإن قائمة النتائج ستكون: [("ab", "123"), ("3ab", "12"), ("23ab", "1")] نحتاج إلى تحليل أطول سلسلة من الأرقام وتحويلها إلى كتابة Int . التنفيذ الكامل هو كما يلي:


 -- int ::= digit{digit} -- digit ::= '0' | ... | '9' intParser :: Parser Int intParser = Parser $ \s -> let res = unParser (some digitParser) s in case res of [] -> [] ((rest, i) : xs) -> [(rest, read i)] where digitParser = predP isDigit 

الطريقة التالية لكتابة تعبير هي استخدام عملية ثنائية. وفقًا لقواعد اللغة ، يجب أن تتضمن شريحة الفتح أولاً شريحة الفتح ، والمعامل الأول ، والمسافة ، ورمز العملية ، ومساحة أخرى ، والمعامل الثاني وقوس الإغلاق. لتحليل الأحرف الفردية (الأقواس والمسافات) ، نستخدم وظيفة charP . المعالجات عبارة عن تعبيرات ، وهناك بالفعل محلل ( exprParser ) exprParser . لتحليل رمز العملية الثنائية ، نحن نصف المحلل المساعد أدناه. يبقى أن نجمع بدقة هذه المجموعة من المحللون. يجب أن يكون هناك أقواس في بداية ونهاية التعبير: يجب عليك التحقق من ذلك ، لكن تجاهل النتيجة نفسها. للقيام بذلك ، استخدم *> و <* :


 binParser :: Parser Expr binParser = charP '(' *> ??? <* charP ')' 

بين هذه BinaryExpr للأقواس ، يجب BinaryExpr تعبير باستخدام مُنشئ BinaryExpr التعبير والتشغيل. لا تنس المساحات المحيطة برمز العملية ، باستخدام نفس الطريقة بالنسبة للأقواس. هذا الجزء هو كما يلي:


 BinaryExpr <$> exprParser --   <*> (charP ' ' *> binOpParser <* charP ' ') -- ,   <*> exprParser --   

نستبدل هذا التعبير بدلاً من علامات الاستفهام:


 -- binOpExpr ::= '(' expr ' ' binOp ' ' expr ')' binParser :: Parser Expr binParser = charP '(' *> (BinaryExpr <$> exprParser <*> (charP ' ' *> binOpParser <* charP ' ') <*> exprParser ) <* charP ')' 

العملية الثنائية هي إما حرف + يوزع قيمة Add ، أو * يوزع Mul :


 -- binOp ::= '+' | '*' binOpParser :: Parser Operator binOpParser = plusParser <|> multParser where plusParser = charP '+' $> Add multParser = charP '*' $> Mul 

ما تبقى هو أبسط جزء من قواعد اللغة ، ونفي التعبير. مع رمز - نفعل الشيء نفسه كما هو الحال مع الأقواس والمسافات. بعد ذلك ، قم بتطبيق مُنشئ NegateExpr على نتيجة التحليل NegateExpr :


 -- negExpr ::= '-' expr negParser = charP '-' *> (NegateExpr <$> exprParser) 

لذلك ، يتم تنفيذ جميع أجزاء المحلل اللغوي. يشبه الكود قواعد نحوية ويتزامن تمامًا معها في البنية.


شفرة المصدر متاحة في GitLab: https://gitlab.com/fierce-katie/applicative-parsers-demo .


هناك أسهل في تقييم حجم ودرجة التعبير ، حيث يوجد عدد أقل بكثير من التعليقات. يمكنك ترجمة المشروع باستخدام الأداة المساعدة Stack وتشغيل مترجم بدائي باستخدام المحلل اللغوي الذي كتبناه:


 $ stack build $ stack exec demo-parser 

بالنسبة لأولئك الذين يرغبون في ممارسة المزيد من التمارين من تلقاء أنفسهم ، يمكنني تقديم المشورة التالية:


  • يمكن تحسين القواعد النحوية بكل طريقة ، على سبيل المثال ، للسماح بالمساحات الأمامية والمعلقة ، وإضافة عمليات جديدة ، إلخ.
  • يقوم المحلل اللغوي بترجمة السلسلة إلى التمثيل الداخلي للتعبير. يمكن حساب هذا التعبير وتحويل المترجم الشفوي بحيث لا يطبع نتيجة التحليل ، ولكن نتيجة الحساب.
  • استكشف إمكانيات applicative-parsec optparse-applicative و optparse-applicative و optparse-applicative و optparse-applicative وحاول استخدامها.

شكرا لاهتمامكم!


مواد مفيدة


  1. تعلم هاسكل في Y دقائق
  2. دينيس شيفتشينكو. "عن هاسكل كإنسان"
  3. مكتبة بارسيك
  4. مكتبة أتوبارسك
  5. مكتبة بارسيك التطبيقية
  6. مكتبة Optparse التطبيقية

Source: https://habr.com/ru/post/ar436234/


All Articles