۲۳ - ۴پارسِ مقادیر کسری
حالا که یه کم با پارسینگ، ترکیبکنندههای پارسر، و پایهی موندی ِ پارسینگ آشنا شدیم، بریم سراغ پارسینگ ِ اعداد کسری. بالای ماژول اینها رو بنویسین:
{-# LANGUAGE OverloadedStrings #-}
module Text.Fractions where
import Control.Applicative
import Data.Ratio ((%))
import Text.Trifectaاسمِ ماژول رو گذاشتیم Text.Fractions چون میخوایم از روی نوشته اعدادِ کسری پارس کنیم. اول با trifecta از ورودیهای String استفاده میکنیم، اما جلوتر میبینین که چرا از توسعه ِ OverloadedStrings استفاده کردیم.
کسر پارس کنیم! با چندتا ورودیِ تست شروع میکنیم:
badFraction = "1/0"
alsoBad = "10"
shouldWork = "1/2"
shouldAlsoWork = "2/1"بعد خودِ پارسر رو مینویسیم:
parseFraction :: Parser Rational
parseFraction = do
numerator <- decimal
-- [2] [1]
char '/'
-- [3]
denominator <- decimal
-- [ 4 ]
return (numerator % denominator)
-- [5] [6]۱.
decimal :: Integral a => Parser aدر بافت ِ اون دو تابع، تایپِ decimal اینطوری میشه. اگه تو GHCi تایپش رو استعلام کنین، تایپ پلیمورفیکتری میبینین.
۲.
اینجا تایپِ numerator از این قراره: Integral a => a.
۳.
char :: Char -> Parser Charمشابهِ decimal، اگه تایپِ char هم توی GHCi استعلام کنین، یه تایپِ پلیمورفیکتری میبینین. اما تو این بافت، char چنین تایپی داره.
۴.
مشابهِ numerator، فقط وقتی یه عددِ صحیح پارس میشه، به اسمِ denominator انقیاد داده میشه.
۵.
نتیجهی آخر باید یه پارسر باشه، پس با استفاده از return، مقدارِ عددی رو میذاریم تو یه تایپِ Parser.
۶.
نسبتها رو با اوپراتور میانوند ِ % درست میکنیم:
(%) :: Integral a
=> a -> a -> GHC.Real.Ratio aو به خاطر اینکه جواب نهاییمون Rational ِه، اون Integral a => a به تایپِ Integer معین میشه.
type Rational = GHC.Real.Ratio Integerیه main ِ کوچیک هم سرِهم میکنیم تا پارسر رو با ورودیهای تست اجرا کنه و نتایج رو ببینیم:
main :: IO ()
main = do
let parseFraction' =
parseString parseFraction mempty
print $ parseFraction' shouldWork
print $ parseFraction' shouldAlsoWork
print $ parseFraction' alsoBad
print $ parseFraction' badFractionنگرانِ اون مقادیرِ mempty نباشین؛ شاید بهتون یه ایدهای از پشتپردهی trifecta بدن، اما این مورد رو تو این فصل بررسی نمیکنیم.
خیلی مختصر به تایپِ parseString، که از طریقش داریم پارسر رو اجرا میکنیم اشاره میکنیم:
parseString :: Parser a
-> Text.Trifecta.Delta.Delta
-> String
-> Result aآرگومانِ اول، پارسرایه که روی ورودی اجرا میکنیم، آرگومانِ دوم یه Delta ست، سومی String ایه که میخوایم پارس کنیم، و نتیجهی آخر هم یا مقداریه که از تایپِ a میخواستیم یا نوشته ِ خطا که میگه یه اشکالی پیش اومد. اون Delta رو میتونین نادیده بگیرین – از mempty استفاده کنین تا یه ورودیِ هیچکاره بهش بدین. در این کتاب دلتاها رو نمیگیم، میتونین به چشمِ امتیاز اضافه نگاهشون کنین.
در هر حال، اگه کُد رو اجرا کنیم چنین نتیجهای میده:
Prelude> main
Success (1 % 2)
Success (2 % 1)
Failure (interactive):1:3: error: unexpected
EOF, expected: "/", digit
10<EOF>
^
Success *** Exception: Ratio has zero denominatorدوتا اولی موفقیتآمیز بودن. سومی شکست خورد چون نتونست یه کسر از نوشته ِ "10" پارس کنه. پیغام خطا میگه نوشتهش تموم شد ولی هیچ کاراکترِ '/' پیدا نکرد. خطای آخری از پارسینگ نبود؛ چون دادهسازِش یه Success ِه. خطای آخر به خاطر این بود که میخواستیم با مخرج ِ صفر یه کسر درست کنیم – که معنایی نداره. همین خطا رو تو GHCi هم میتونیم درست کنیم:
Prelude> 1 % 0
*** Exception: Ratio has zero denominator
-- پس در واقع جواب پارسر معادل است با
Prleude> Success (1 % 0)
Success *** Exception: Ratio has zero denominatorیه جورایی مشکلسازه چون استثناها برنامه رو از کار میندازن. ببینین:
main :: IO ()
main = do
let parseFraction' =
parseString parseFraction mempty
print $ parseFraction' badFraction
print $ parseFraction' shouldWork
print $ parseFraction' shouldAlsoWork
print $ parseFraction' alsoBadاین دفعه بیانیهای که استثنا میده رو گذاشتیم خطِ اول. اگه اجرا کنیم:
Prelude> main
Success *** Exception: Ratio has zero denominatorپس برنامهمون سرِ خطا متوقف شد. خوب نیست. شاید وسوسه بشین که خطا رو "مدیریت" کنین. گرفتن ِ استثناها موردی نداره، اما این جزئی از دسته خطاهاییه که نشون میده برنامه یه مشکلِ اساسی داره. هرجا که ممکنه باید احتمالِ استثناها رو از برنامههاتون حذف کنین.
در یکی از فصلهای بعدی از مدیریت خطا بیشتر صحبت میکنیم، اما مسئلهی اینجا اینه که یه تایپِ Parser صراحتاً احتمالِ شکست رو کدبندی کرده. بهتره که یک مقدار از تایپِ Parser a فقط یک راه برای شکست داشته باشه، و اون هم همون قابلیت پارسر برای کدبندی ِ شکست باشه. شاید یه حالت مرزی باشه که نشه چنین راهحلی رو براش پیاده کرد، اما اینکه تا جای ممکن هیچ استثنا یا تهیای که صراحتاً در تایپها مشخص نشده نداشته باشیم ایدهی خیلی خوبیه.
میشه برنامهمون رو تغییر بدیم تا مخرج ِ صفر رو در نظر بگیره و به یه خطای پارس تغییرش بدیم:
virtuousFraction :: Parser Rational
virtuousFraction = do
numerator <- decimal
char '/'
denominator <- decimal
case denominator of
0 -> fail "Denominator cannot be zero"
_ -> return (numerator % denominator)اینجا برای اولین بار اینطور صراحتاً از fail استفاده کردیم، که حالا (به خاطر یه تصادفِ تاریخی) جزئی از تایپکلاسِ Monad شده. در حقیقت همهی Monadها یه تعریفِ مناسب برای fail ندارن، پس انتظار میره که نهایتاً به تایپکلاسِ MonadFail منتقل بشه. فعلاً کفایت میکنه که به عنوان یه راهی برای برگردوندنِ خطا در تایپِ Parser ببینینش.
حالا یه بارِ دیگه ورودیهامون رو تست کنیم؛ این بار با پارسر ِ جدیدمون:
testVirtuous :: IO ()
testVirtuous = do
let virtuousFraction' =
parseString virtuousFraction mempty
print $ virtuousFraction' badFraction
print $ virtuousFraction' alsoBad
print $ virtuousFraction' shouldWork
print $ virtuousFraction' shouldAlsoWorkجوابی که از اجراش میگیریم یه ذره فرق داره:
Prelude> testVirtuous
Failure (interactive):1:4: error: Denominator
cannot be zero, expected: digit
1/0<EOF>
^
Failure (interactive:1:3: error unexpected
EOF, expected "/", digit
10<EOF>
^
Success (1 % 2)
Success (2 % 1)حالا دیگه هیچ تهیای نداریم که باعث بشه برنامهمون متوقف بشه، و یه مقدارِ Failure میگیریم که عاملِ شکست رو توضیح میده. خیلی بهتر شد!
تمرین: واحد موفقیت
حتی اگه همهی جزئیات رو متوجه نمیشین، نباید براتون ناآشنا باشه:
λ> parseString integer mempty "123abc"
Success 123
λ> parseString (integer >> eof) mempty "123abc"
Failure (interactive):1:4: error: expected: digit,
end of input
123abc<EOF>
^
λ> parseString (integer >> eof) mempty "123"
Success ()احتمالاً خودتون حدس زدین که چرا جوابش Success () شد؛ تمامِ ورودی رو مصرف کرد اما نتیجهای برای برگردوندن نداره. جوابِ Success () یعنی پارس موفقیتآمیز بود و همهی ورودی مصرف شد، پس چیزی برای برگردوندن نیست.
کاری که میخوایم انجام بدین اینه که مثالِ آخر رو طوری بازنویسی کنین تا بجای Success ()، عددِ صحیحی که پارس کرده رو برگردونه. با ورودیهایی که یه عدد و به دنبالش EOF هستن، باید با موفقیت عددِ صحیح رو برگردونه و در هر حالتِ دیگهای شکست بخوره:
λ> parseString (yourFuncHere) mempty "123"
Success 123
λ> parseString (yourFuncHere) mempty "123abc"
Failure (interactive):1:4: error: expected: digit,
end of input
123abc<EOF>
^