۲۳ - ۶اَلتِرنِتیو یا Alternative
فرض کنیم یه پارسر برای اعداد و یه پارسر برای نوشتههای الفباعددی داشته باشیم:
Prelude> import Text.Trifecta
Prelude> parseString (some letter) mempty "blah"
Success "blah"
Prelude> parseString integer mempty "123"
Success 123اگه یه تایپی داشتیم که میتونست هم Integer باشه هم String، چطور؟
module AltParsing where
import Control.Applicative
import Text.Trifecta
type NumberOrString =
Either Integer String
a = "blah"
b = "123"
c = "123blah789"
parseNos :: Parser NumberOrString
parseNos =
(Left <$> integer)
<|> (Right <$> some letter)
main = do
let p f i =
parseString f mempty i
print $ p (some letter) a
print $ p integer b
print $ p parseNos a
print $ p parseNos b
print $ p (many parseNos) c
print $ p (some parseNos) cعملگر ِ <|> رو میشه "یا"، یا فصلِ منطقی بین دو پارسر خوند؛ many معادلِ صفر یا بیشتر، و some معادل یکی یا بیشتره.
Prelude> parseString (some integer) mempty "123"
Success [123]
Prelude> parseString (many integer) mempty "123"
Success [123]
Prelude> parseString (many integer) mempty ""
Success []
Prelude> parseString (some integer) mempty ""
Failure (interactive):1:1: error: unexpected
EOF, expected: integer
<EOF>
^توابع some، many و (<|>) که اینجا استفاده کردیم از تایپکلاسی به اسمِ Alternative (م. به معنای "جایگزین") هستن:
class Applicative f => Alternative f where
-- | <|> همانی برای
empty :: f a
-- | یه اوپراتور باینری و شرکتپذیر
(<|>) :: f a -> f a -> f b
-- | .یکی یا بیشتر
some :: f a -> f [a]
some v = some_v
where
many_v = some_v <|> pure []
some_v = (fmap (:) v) <*> many_v
-- | .صفر یا بیشتر
many :: f a -> f [a]
many v = many_v
where
many_v = some_v <|> pure []
some_v = (fmap (:) v) <*> many_vاگه بعد از وارد کردنِ Text.Trifecta یا بارگذاری ِ ماژول ِ بالا در REPL از دستورِ :info استفاده کنین، میبینین که some و many در GHC.Base تعریف شدن، چون این تایپکلاس مختصِ هیچ پارسر ِ بخصوص یا کتابخونه ِ parsers، یا اصلاً این مسئلهی خاص نیست.
اگه بخوایم ملزم کنیم هر مقدار با یه خطِجدید از هم جدا شده باشه چطور؟ با QuasiQuotes میشه نوشتههای چندخطی رو بدونِ نوشتنِ کاراکترِ خطِجدید درست کرد و به عنوانِ یه آرگومان ازشون استفاده کرد:
{-# LANGUAGE QuasiQuotes #-}
module AltParsing where
import Control.Applicative
import Text.RawString.QQ
import Text.Trifecta
type NumberOrString =
Either Integer String
eitherOr :: String
eitherOr = [r|
123
abc
456
def
|]QuasiQuotes
در کُدِ بالا، اون [r| یه بخشِ نیمنَقلی* رو با نیمنقلکنندهای به اسمِ r شروع میکنه. دقت کنین که برای استفاده از این گرامر باید توسعهی زبانی ِ QuasiQuotes رو فعال میکردیم. در زمان نوشتار این کتاب، r در raw-strings-qq نسخهی ۱٫۱ به صورتِ زیر تعریف شده:
r :: QuasiQuoter
r = QuasiQuoter {
-- dead-simple-json استخراج شده از
quoteExp =
return . LitE . StringL
. normaliseNewlines,
-- پیغامهای خطا رو ننوشتیم
quotePat =
\_ -> fail "some error message"
quoteType =
\_ -> fail "some error message"
quoteDec =
\_ -> fail "some error message"صفحهی wiki ِ خوبی با مثال در این آدرس هست.
این در واقع یه روش برای نوشتنِ متونِ دلخواه داخل یه بلوکی که با |r] شروع و با [| تموم میشهست. این نیمنقلکننده ِ بخصوص برای نوشتنِ متونِ چندخطی، بدونِ نیاز به دستی نوشتنِ کاراکترِ \n کاربرد داره. نیمنقلکنندهای که بالاتر نوشتیم چنین نوشتهای ایجاد میکنه:
"\n\
\123\n
\abc\n
\456\n
\def\n"به خوشگلیِ قبلش نیست، نه؟ اگه بخواین خروجیِ نیمنقلکننده یا Template Haskell رو ببینین میتونین پرچم ِ -ddump-splices رو فعال کنین. یه مثالِ ساده:
{-# LANGUAGE QuasiQuotes #-}
module Quasimodo where
import Text.RawString.QQ
eitherOr :: String
eitherOr = [r|
123
abc
456
|]بعد اگه تو GHCi اون پرچم رو با :set روشن کنیم، میشه چیزی که نیمنقلکننده ایجاد میکنه رو ببینیم:
Prelude> :set -ddump-splices
Prelude> :l code/quasi.hs
[1 of 1] Compiling Quasimodo
code/quasi.hs:(8,12)-(12,2): Splicing expression
"\n\
\123\n\
\abc\n\
\456\n"
======>
"\n\
\123\n\
\abc\n\
\456\n"خیلی خوب، برگردیم به پارسری که میخواستیم بنویسیم!
بازگشت به Alternative
برمیگردیم به ماژول ِ AltParsing. میخوایم از این تابعِ فوقالعاده استفاده کنیم:
parseNos :: Parser NumberOrString
parseNos =
(Left <$> integer)
<|> (Right <$> some letter)و main رو طوری بازنویسی کنیم که اون رو به مقدارِ eitherOr اعمال کنه:
main = do
let p f i = parseString f mempty i
print $ p parseNos eitherOrدقت کنین که Left و Right رو از روی آرگومانهاشون لیفت کردیم. دلیلش اینه که بین چیزی که اون دادهسازها انتظار دارن و مقداری که (احتمالاً) از اجرای پارسر بدست میاد، ساختار ِ Parser وجود داره. یه مقدار با تایپِ Parser Char پارسِر ایه که اگه بهش ورودیای داده بشه که منجر به شکستِش نشه، یه مقدارِ Char تولید میکنه. تایپِ some letter از این قراره:
Prelude> import Text.Trifecta
Prelude> :t some letter
some letter :: CharParsing f => f [Char]ولی برای ما کفایت میکنه که بگیم تایپش همون تایپِ Parser ِ مختص به trifecta ست.
λ> let someLetter = some letter :: Parser [Char]
λ> let someLetter = some letter :: Parser Stringاگه بخوایم مثل یه بچهای که با اسباببازیهاش بازی میکنه، پارسر ِ نوشته رو به زور به یه دادهسازی که انتظارِ String داره بدیم، خطای تایپ میگیریم:
λ> data MyName = MyName String deriving Show
λ> MyName someLetter
Couldn't match type ‘Parser String’ with ‘[Char]’
Expected type: String
Actual type: Parser String
In the first argument of ‘MyName’,
namely ‘someLetter’
In the expression: MyName someLetterمگر اینکه از روی ساختار ِ Parser لیفتِش کنیم، چون Parser یه Functor ِه!
λ> :info Parser
{... خلاصه کردیم ...}
instance Monad Parser
instance Functor Parser
instance Applicative Parser
instance Monoid a => Monoid (Parser a)
instance Errable Parser
instance DeltaParsing Parser
instance TokenParsing Parser
instance CharParsing Parserبا یه fmap درست میشه، قبوله؟
λ> :t MyName <$> someLetter
MyName <$> someLetter :: Parser MyName
λ> :t MyName `fmap` someLetter
MyName `fmap` someLetter :: Parser MyNameبعد با اجرای هرکدوم از اونها:
λ> parseString someLetter mempty "Chris"
Success "Chris"
λ> let mynameParser = MyName <$> someLetter
λ> parseString mynameParser mempty "Chris"
Success (MyName "Chris")خیلی هم خوب.
برگردیم به کُدِ اصلیمون که خطا میده:
λ> main
Failure (interactive):1:1: error: expected: integer,
letterاگه به نوشته ِ تستمون نگاه کنیم، راحتتر میشه:
λ> eitherOr
"\n123\nabc\n456\ndef\n"یه راهش اینه که نوشته ِ نیمنقلشده رو درست کنیم:
eitherOr :: String
eitherOr = [r|123
abc
456
def
|]ولی اگه میخواستیم اجازهی وجودِ یه خطِجدید قبل از شروع به پارس ِ نوشتهها یا اعداد رو بدیم چطور؟
eitherOr :: String
eitherOr = [r|
123
abc
456
def
|]
parseNos :: Parser NumberOrString
parseNos =
skipMany (oneOf "\n")
>>
(Left <$> integer)
<|> (Right <$> some letter)
main = do
let p f i = parseString f mempty i
print $ p parseNos eitherOr
Prelude> main
Success (Left 123)اوکِی، ولی میخوایم بعد از هر خط به پارسکردن ادامه بدیم. اگه چارهی واضح رو انتخاب کنیم و از some استفاده کنیم تا یک-یا-بیشتر جواب بخوایم، یه خطای نسبتاً عجیب میگیریم:
λ> parseString (some parseNos) mempty eitherOr
Failure (interactive):6:1: error: unexpected
EOF, expected integer, letter
<EOF>
^اینجا با skipMany کاراکترِ '\n' رو، صفر بار یا بیشتر رَد میکنیم، پس معنیش اینه که اجرای بعدیِ پارسر رو، قبل از رسیدن به EOF شروع میکنیم. یعنی بعد از "def" انتظارِ یه عدد صحیح یا تعدادی حروف رو داره. میشه ورودی رو درست کنیم:
eitherOr :: String
eitherOr = [r|
123
abc
456
def|]حالا کُدِ قبلی کار میکنه:
λ> parseString (some parseNos) mempty eitherOr
Success [Left 123,Right "abc",Left 456,Right "def"]اگه با این کار راضی نمیشیم، دو راه داریم تا پارسر رو با خطِجدیدهای پایانی سازگار کنیم. یکیش اینه که یه skipMany ِ دیگه بعد از پارس اضافه کنیم:
parseNos :: Parser NumberOrString
parseNos = do
skipMany (oneOf "\n")
v <- (Left <$> integer)
<|> (Right <$> some letter)
skipMany (oneOf "\n")
return vیه راهِ دیگه اینه که نسخهی قبلیِ پارسر ِمون (که خطجدیدهای احتمالیِ اولِ نوشته رو رَد میکنه) رو نگه داریم:
parseNos :: Parser NumberOrString
parseNos =
skipMany (oneOf "\n")
>>
(Left <$> integer)
<|> (Right <$> some letter)ولی بعد با رفتارِ پیشفرضِ token، نشانهگذاریش کنیم:
> parseString some (token parseNos)) mempty eitherOr
uccess [Left 123,Right "abc",Left 456,Right "def"]به زودی توکِن رو توضیح میدیم، ولی باید حواسمون رو جمع کنیم چون پارسرهای توکن و پارسرهای کاراکتر چیزهای متفاوتیاند. کاری که اینجا اعمالِ token به parseNos برامون انجام داد این بود که در صورت وجودِ فاصلهی سفیدِ انتهایی اونها رو هم مصرف کنه؛ خطِجدید هم یکی از فاصلههای سفید هست.
تمرین: try امتحان کنین
با پارسرِ کسری که داشتیم، به علاوهی یه پارسر ِ جدید برای اعدادِ دهدهی، یه پارسری درست کنین که بتونه هم اعداد دهدهی و هم اعداد کسری رو پارس کنه. عملگر ِ <|> از Alternative رو لازم دارین تا اون دوتا پارسر ِ جایگزین همدیگه رو با هم ترکیب کنین. اگه به نظرتون خیلی سخت اومد، یه پارسر درست کنین که بتونه هم اعدادِ صحیح و هم اعدادِ کسریای که سَرراست هستن رو پارس کنه. یه نوعداده درست کنین که شامل اعداد صحیح یا گویا باشه و ازش به عنوان جوابِ پارسر استفاده کنین. یا از Either استفاده کنین. خودتون مختارین.
راهنمایی: هنوز توضیح ندادیم، ولی شاید بخواین try رو امتحان کنین.