۲۳ - ۴پارسِ مقادیر کسری
حالا که یه کم با پارسینگ، ترکیبکنندههای پارسر، و پایهی موندی ِ پارسینگ آشنا شدیم، بریم سراغ پارسینگ ِ اعداد کسری. بالای ماژول اینها رو بنویسین:
{-# 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>
^