۲۳ - ۹پارسرهای پلیمورفیک
اگه به پارسرها تایپهای پلیمورفیک بدیم، اون موقع پارسرهایی داریم که میشه با 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>
^
حالا پیغام خطا همهی پارسهایی که قبل از شکست امتحان شدن رو لیست کرده. در عمل بهتره از برچسبهای بهتری برای پارسرهاتون استفاده کنین.