۲۳ - ۹پارسرهای پلی‌مورفیک

اگه به پارسرها تایپ‌های پلی‌مورفیک بدیم، اون موقع پارسرهایی داریم که میشه با ‏‎attoparsec‎‏، ‏‎trifecta‎‏، ‏‎parsec‎‏، و هر چیزِ دیگه‌ای که تایپکلاس‌های مناسب رو داره ازشون استفاده کنیم. یه امتحان کنیم، قبوله؟

{-# LANGUAGE OverloadedStrings #-}

module Text.Fractions where

import Control.Applicative
import Data.Attoparsec.Text (parseOnly) 
import Data.Ratio ((%))
import Data.String (IsString) 
import Text.Trifecta

badFraction :: IsString s => s
badFraction = "1/0"

alsoBad :: IsString s => s
alsoBad = "10"

shouldWork :: IsString s => s
shouldWork = "1/2"

shouldAlsoWork :: IsString s => s
shouldAlsoWork = "2/1"

parseFraction :: (Monad m, TokenParsing m)
              => m Rational
parseFraction = do
  numerator <- decimal
  _ <- char '/'
  denominator <- decimal
  case denominator of
    0 -> fail "Denominator cannot be zero"
    _ -> return (numerator % denominator)

چندتا از محدودیت‌های تایپکلاسی رو نیاوردیم تا انعطاف‌پذیری بیشتری داشته باشه. ‏‎main‎‏ این پارسر رو هم با ‏‎attoparsec‎‏ و هم با ‏‎trifecta‎‏ اجرا می‌کنه تا بتونیم خروجی‌هاشون رو مقایسه کنیم:

main :: IO ()
main = do
  -- Attoparsec از parseOnly
  let attoP = parseOnly parseFraction
  print $ attoP badFraction
  print $ attoP shouldWork
  print $ attoP shouldAlsoWork
  print $ attoP alsoBad

  -- Trifecta از parseString
  let p f i =
        parseString f mempty i
  print $ p parseFraction badFraction
  print $ p parseFraction shouldWork
  print $ p parseFraction shoulAlsoWork
  print $ p parseFraction alsoBad
Prelude> main
Left "Failed reading: Denominator cannot be zero"
Right (1 % 2)
Right (2 % 1)
Left "\"/\": not enough input"
Failure (interactive):1:4: error: Denominator
    cannot be zero, expected: digit
1/0<EOF>
   ^
Success (1 % 2)
Success (2 % 1)
Failure (interactive):1:3: error: unexpected
    EOF, expected: "/", digit
10<EOF>
  ^

یادتون میاد راجع به پیغام‌های خطا چی گفتیم؟

ایده‌آل نیست و ممکنه گازِتون بگیره

با اینکه با ترکیب‌کننده‌های پارسرِ پلی‌مورفیک در کتابخونه ِ ‏‎parsers‎‏ میشه پارسرهایی نوشت که با کتابخونه‌های پارسینگ ِ متنوعی کار می‌کنن، ولی معنی‌ش این نیست که نیازی به آشنایی با جزئیاتِ هرکدوم نباشه. به طور کلی، در اکثرِ جهات ‏‎trifecta‎‏ رفتارش رو با ‏‎parsec‎‏ منطبق کرده، ولی ‏‎parsec‎‏ مستنداتِ خیلی کامل‌تری داره.

شکست و بازبینی

اگه برگردیم به توصیفی که پارسرها رو مثل خوندنِ با انگشت معرفی کردیم، بازبینی یعنی برگردوندنِ اون نشانگر به جایی که قبل از شکست ِ پارسر قرار داشته. در بعضی مواقع، مثلِ زیر، رفع‌اشکالِ یک خطا در دو پارسری که اساساً یک کار رو انجام میدن ممکنه گیج‌کننده باشه. در مثالِ زیر این کار رو با ‏‎trifecta‎‏، ‏‎parsec‎‏، و ‏‎attoparsec‎‏ امتحان می‌کنیم.

{-# LANGUAGE OverloadedStrings #-}

از توسعه ِ ‏‎OverloadedStrings‎‏ استفاده می‌کنیم تا بتونیم موقعِ تستِ ‏‎attoparsec‎‏، از لفظ‌های نوشته به عنوانِ ‏‎ByteString‎‏ استفاده کنیم:

module BT where

import Control.Applicative
import qualified Data.Attoparsec.ByteString
  as A
import Data.Attoparsec.ByteString
  (parseOnly)
import Data.ByteString (ByteString) 
import Text.Trifecta hiding (parseTest)
import Text.Parsec (Parsec, parseTest)

تابع کمکی برای اجرای پارسر ِ ‏‎trifecta‎‏ و چاپ ِ نتیجه‌ش:

trifP :: Show a
      => Parser a
      -> String -> IO ()
trifP p i =
  print $ parseString p mempty i

تابع کمکی برای اجرای پارسر ِ ‏‎parsec‎‏ و چاپ ِ نتیجه‌ش:

parsecP :: Show a
      => Parsec String () a
      -> String -> IO ()
parsecP = parseTest

مثل بقیه، تابع کمکی برای پارسر ِ ‏‎attoP‎‏:

attoP :: Show a
      => A.Parser a
      -> ByteString -> IO ()
attoP p i =
  print $ parseOnly p i

این اولین پارسر‌ِمونه که سعی می‌کنه ‏‎'1'‎‏ و به دنبال‌ش ‏‎'2'‎‏، یا ‏‎'3'‎‏ رو پارس کنه. این پارسر بازبینی نمی‌کنه:

nobackParse :: (Monad f, CharParsing f)
            => f Char
nobackParse =
       (char '1' >> char '2')
   <|> char '3'

این پارسر هم رفتارِ مشابهی داره، با این تفاوت که در صورتِ شکستِ اولین پارسر، بازبینی می‌کنه. بازبینی یعنی اون نشانگر ِ ورودی برمی‌گرده به جایی که قبل از مصرف ِ ورودی توسطِ پارسر ِ شکست‌خورده قرار داشت:

tryParse :: (Monad f, CharParsing f)
         => f Char
tryParse =
       try (char '1' >> char '2')
   <|> char '3'

‏‎main‎‏ رو ببینیم:

main :: IO ()
main = do
  -- trifecta
  trifP nobackParse "13"
  trifP tryParse "13"
  -- parsec
  parsecP nobackParse "13"
  parsecP tryParse "13"
  -- attoparsec
  attoP nobackParse "13"
  attoP tryParse "13"

پیغام خطایی که از هر پارسر می‌گیرین، کمی با هم فرق می‌کنن. این تفاوت هم بیشتر به خاطر نحوه‌ی ارائه‌ی خطاها در پارسرهاست:

Prelude> main
Failure (interactive):1:2:
  error: expected: "2"
13<EOF>
 ^
Failure (interactive):1:1: error:
  expected: "3"
13<EOF>
^
parse error at (line 1, column 2):
unexpected "3"
expecting "2"
parse error at (line 1, column 2):
unexpected "3"
expecting "2"
Left "\"3\": satisfyElem"
Left "\"3\": satisfyElem"

اگه هر سه‌تا پارسر رو با ورودی‌های ‏‎"12"‎‏ و ‏‎"3"‎‏ و پارسر ِ ‏‎nobackParse‎‏ امتحان کنین، می‌بینین همه‌شون موفق میشن.

ممکنه گیج‌کننده باشه. وقتی بازبینی به یه پارسر اضافه می‌کنین، پیغام‌های خطا یه کم پیچیده‌تر میشن. برای اینکه به چنین مشکلی نخورین، هر وقت که از ‏‎try‎‏ استفاده می‌کنین، با عملگر ِ ‏‎<?>‎‏ هر پارس رو با یه برچسب مشخص کنین.*

*

به گفته‌ی اِدوارد یَنگ، استفاده از ‏‎try a <|> b‎‏ در ‏‎parsec‎‏ مُضِرِه.

tryAnnot :: (Monad f, CharParsing f)
         => f Char
tryAnnot =
       (try (char '1' >> char '2')
       <?> "Tried 12")
   <|> (char '3' <?> "Tried 3")

این رو تو REPL اجرا کنیم:

Prelude> trifP tryAnnot "13"
Failure (interactive):1:1: error: expected:
    Tried 12, Tried 3
13<EOF>
^

حالا پیغام خطا همه‌ی پارس‌هایی که قبل از شکست امتحان شدن رو لیست کرده. در عمل بهتره از برچسب‌های بهتری برای پارسرهاتون استفاده کنین.