۲۳ - ۴پارسِ مقادیر کسری

حالا که یه کم با پارسینگ، ترکیب‌کننده‌های پارسر، و پایه‌ی موندی ِ پارسینگ آشنا شدیم، بریم سراغ پارسینگ ِ اعداد کسری. بالای ماژول اینها رو بنویسین:

{-# 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>
   ^