۲۳ - ۳درکِ فرایند پارسکردن
پارسر یه تابعه که یه ورودیِ نوشتاری میگیره (ممکنه String باشه، یا نوعدادهای دیگه مثل ByteString یا Text) و یه ساختار رو به عنوان خروجی برمیگردونه. اون ساختار برای مثال ممکنه یه درخت، یا یه نگاشت اندیسدار از مکانهای مختلف در داده ِ پارسشده باشه. پارسرها با توجه به قواعدِ تعیین شده در یه گرامر (چه گرامر ِ زبان انسانی، زبان برنامهنویسی، یا یه فرمت مثلِ JSON)، ساختار رو بررسی میکنن.
یک ترکیبکنندهی پارسر، یه تابعِ سطحبالا هست که به عنوانِ ورودیهاش پارسر میگیره و یه پارسر ِ جدید خروجی میده. ممکنه توضیحِ کوتاهی که از ترکیبکنندهها در فصلِ جبرِ لاندا دادیم خاطرتون باشه. ترکیبکنندهها بیانیههاییاند که هیچ متغیر آزادی ندارن.
پارسر تابعه، پس ترکیبکنندهی پارسر یه تابعِ سطحبالا ِه که آرگومانهاش پارسِر اند. واسط تعاملی ِ ترکیبکنندهها معمولاً شبیهِ موند ِ Stateهاست، یعنی دادنِ آرگومانها میتونه به سبکِ Reader و ضمنی باشه. ترکیبکنندهها امکانِ خوداتکایی و چسبوندنِ ماژولار ِ پارسرها به همدیگه رو دارن تا برمبنای قواعدی پیچیده دادهها رو پارس کنن.
میشه پارسکردن برای کامپیوترها رو مشابهِ خوندنِ شما در دورانِ کودکیتون دونست. احتمالاً بهتون یاد داده بودن در حین خوندن، با انگشتتون حروف رو دنبال کنین. بعداً بجای حروف، لغت به لغت پیش میرفتین، بعد از اون هم با چشمهاتون دنبال میکردین. آخر سر هم یاد گرفتین ذهنی بخونین.
قیاس برای Monad
اینجا ایدهی پارسینگ رو با چندتا کُد نشون میدیم. اول با نصبِ کتابخونه ِ trifecta* کار رو شروع میکنیم، و بعد یه مثالِ کوتاه از طرزِ کارش میزنیم. فعلاً یه کم با بیخیالی باهاش کار میکنیم، اما جلوتر از طراحیِ trifecta بیشتر صحبت میکنیم.
ما از این نسخهی trifecta استفاده میکنیم.
کد بنویسیم:
module LearnParsers where
import Text.Trifecta
stop :: Parser a
stop = unexpected "stop"با unexpected میشه در پارسرهایی مثل trifecta که نمونهای از تایپکلاسِ Parsing هستن خطا داد. اینجا به منظور نمایش میخوایم کاری کنیم که پارسر شکست بخوره.
نمایشِ چی؟
مرسی که پرسیدین! ایدهی کلیِ پارسر، حرکتِ یه جور نشانگر روی یه جریان ِ خطی از نوشتاره. در سادهترین حالت، هر واحد مجرد از این جریان یک حرف یا اندیشهنگاشت* ِه، ولی جلوتر که پیش میرین، بهتره نگرشتون به مسائلِ پارسینگ کلیتر بشه. این نشانگر شبیهِ خوندن با انگشت میمونه:
Julie bit Papuchon
^م. اندیشهنگاشت یا ideograph علائمی هستن که از خودشون صدایی ندارن و برای نشون دادنِ یه جور مفهوم به کار میرن؛ اعداد از مثالهای اونها هستن (ایموجیها هم همینطور).
حالا فرض کنیم لغتِ Julie رو پارس کردیم – اون ورودی رو مصرف کردیم، پس نشانگر میره روی bit:
Julie bit Papuchon
^اگه اینجا انتظارِ bit رو نداشتیم، پارسر شکست میخورد و سرِ لغتِ bit پیغامِ خطا میگرفتیم. ولی اگه با موفقیت bit رو پارس و متعاقباً اون ورودی رو مصرف میکردیم، اینطوری میشد:
Julie bit Papuchon
^چنین قیاسی خیلی دقیق نیست. یکی از سختترین مسائل در نوشتنِ پارسرها، بخصوص خودِ کتابخونههای پارسر، اینه که برنامهنویس همه چیز رو اونطور که براش مطلوبه به سادگی بیان کنه و در عین حال پارسر ِ نهایی سریع هم باشه.
برگردیم به کُد
با در نظر داشتنِ اون قیاسی که با یه نشانگر گفتیم، برگردیم به ماژولی که شروع کردیم.
اول یه تابعِ کوچولو درست میکنیم که فقط یک کاراکتر رو پارس میکنه، بعد اون رو با stop سکانس میکنیم (م. یا تسلسل میدیم. به عبارتِ دیگه، stop رو میذاریم جلوی اون پارسر) تا یک حرف رو بخونه و بعد بمیره:
-- رو بخون '1' یک کارکتر مجرد
one = char '1'
-- رو بخون، بعد بمیر '1' یک کاراکتر مجرد
one' = one >> stop
-- char '1' >> stop معادل بابرای one' از عملگر ِ تسلسل از Monad برای ترکیب ِ دو پارسر ِ stop و char '1' استفاده کردیم. با توجه به تایپِ (>>):
(>>) :: Monad m => m a -> m b -> m bمیشه قاطعانه فرض کرد که هرچی char '1' از بیانیهی:
char '1' >> stopبرمیگردونه دور انداخته میشه. اما هر اثری که اجراییه ِ m a روی بافت ِ موندی میذاره باقی میمونه. به عبارتِ دیگه مقدارِ حاصل از تابعِ پارس دور انداخته میشه، اما اثر ِ "حرکت دادنِ نشانگر" باقی میمونه. "باعثِ شکست ِ پارسر شدن،" یکی دیگه از اثراتِ احتمالیه.
یه کم شبیه...
State. به علاوهی شکست. جدی میگیم، یه نگاه به تعریفِ تایپِ Parser بندازین:
type Parser a = String -> Maybe (a, String)این رو میشه اینطوری خوند:
۱.
منتظر یه مقدارِ String باش.
۲.
جوابی بده که شاید موفقیتآمیز باشه، شاید هم نباشه (مقدارِ Nothing یعنی پارس شکست خورده).
۳.
یه توپل از مقدارِ مورد نظر، و باقیموندهی نوشته که برای تولیدِ مقدارِ تایپِ a مصرف نشد برگردون.
حالا تایپهای Reader و State رو به خاطر بیارین:
newtype Reader r a =
Reader { runReader :: r -> a }
newtype State s a =
State { runState :: s -> (a, s) }اگه متقاعد شدین که State یه جور بسط از Reader ِه، و شباهتِ Parser با State هم میبینین، ادامه بدیم.
ایدهی تایپِ Parser اینه که با State یه ورودیِ نوشتاری قبول میکنه، و پارس ِ یه چیزی از اون نوشته منجر به یه حالت ِ جدید از جریانِ ورودی میشه؛ اون مقدارِ خروجی از حالت مستقله، و Maybe هم احتمالِ شکستِ پارسر رو به عهده داره.
اگه به پشتپردهی یه تابعِ پارسینگ مثلِ char نگاه کنیم، الگویی مشابهِ State میبینیم (فقط دقت کنین با اینکه این کد به عنوان یه تابعِ پارس ِ کاراکتر میتونه کار کنه، خیلی سادهست و کد منبع ِ هیچ کتابخونه ِ پارسینگ ِ مُدِرنی این شکلی نیست):
-- ؛char پیادهسازی ابتدایی برای
-- فقط برای نمونه، در همین
-- .وضعیت کار نمیکنه
char :: Char -> Parser Char
char c =
Parser $ \s ->
case s of
(x:xs) -> if c == x
then [(c, xs)]
else []
_ -> []میشد با Maybe احتمال شکست هم اضافه کنیم، ولی مهم نیست چون میخوایم از کتابخونهای استفاده کنیم که خودش این کار رو برامون انجام داده؛ char هم حسابی بهینه کرده. فقط میخواستیم نشون بدیم که چطور تابع پشتپردهی s -> زیرِ Parser قرار گرفته.
تایپِ پارسر ِ هاتون-مِیِر رو در نظر بگیرین:
-- Text.ParserCombinators.HuttonMeijer از
-- polyparse-1.11
type Token = Char
newtype Parser a =
P ([Token] -> [(a, [Token])])
-- :همونه، فقط فرمتش عوض شده
type Parser' a = String -> [(a, String)]با این پارسر، برخلاف پارسر ِ قبلی (که سادهتره و رواجِ کمتری هم داره) میشه یه تعدادی از پارسهای معتبرِ حاصل از ورودی رو گرفت. این پارسر از مدلِ Maybe قویتره، اما دیگه از این طرح برای ترکیبکنندههای پارسر در کتابخونههای محبوبِ هسکل استفاده نمیشه. با اینکه پیشرفت و کشفِ طرحهای جدید باعث تغییرات زیادی در کتابخونههای پارسینگ شدن، رابط تعاملی ِ بیشتر اونها در هسکل رفتاری شبیهِ State داره؛ از این لحاظ که پارس ِ هر چیزی، روی یک یا چند بخش از حالت یه اثر ِ قابل مشاهده میذاره.
معادل چنین چیزی در State اینه که هر put روی یه مقدارِ State، برای اجراییه ِ بعدی در همون Monad قابل مشاهده باشه (برای امتحان کردنِ کُدِ زیر تو REPL، اول Control.Monad.Trans.State رو وارد کنین). این مثالها از مدلِ ترانسفورمر ِ State استفاده میکنن، اما اگه اون T رو نادیده بگیرین، ایدهی کلی رو یاد میگیرین:
get :: Monad m => StateT s m s
put :: Monad m => s -> StateT s m ()
runStateT :: StateT s m a -> s -> m (a, s)Prelude> runStateT (put 8) 7
((),8)
Prelude> runStateT get 8
(8,8)
Prelude> runStateT (put 1 >> get) 8
(1,1)
Prelude> (runStateT $ put 1 >> get) 0
(1,1)
Prelude> (runStateT $ put 2 >> get) 10021490234890
(2,2)
Prelude> (runStateT $ put 2 >> return 9001) 0
(9001,2)حالا put یه مقدارِ واحد برمیگردونه (یه مقدارِ دورانداختنی) پس درهرصورت فقط برای اثری که میذاره حسابش میکنیم. حالت رو تغییر میده ولی هیچ مقداری از خودش نداره. پس وقتی مقدارش رو دور میندازیم فقط اثرش روی حالت باقی میمونه. از طرفِ دیگه get اون مقدار رو بجای هردو جاهای a و s در توپل جاگذاری میکنه.
چنین چیزی خیلی شبیه وقتهاییه که مثل بالا یه تابعِ پارسینگ مثلِ char رو با stop سکانس میکنیم. هیچ جوابِ واقعیای از char بدست نمیاد، ولی حالت رو تغییر میده. اینجا حالت، مکانِ نشانگر در جریان ِ ورودیه. راستش رو بخواین، یه طرحِ مدرن و پختهی پارسر در هسکل همونقدر براتون آشنایی داره که یکی از غارهای زیر پوستهی یخزدهی یکی از ماههای مشتری براتون آشناست. خیلی اون غار رو جدی نگیرین، ولی احتمالاً اینطوری فرضکردن یه فایدهای داشته باشه.
برگردیم به نظم سابقمون در کدنویسی
به سوی کدنویسی:
-- '2' و '1' دوتا کاراکتر بخون،
oneTwo = char '1' >> char '2'
-- دوتا کاراکتر بخون،
-- ، بعد بمیر'2' و '1'
oneTwo' = oneTwo >> stop
testParse :: Parser Char -> IO ()
testParse p =
print $ parseString p mempty "123"آرگومانِ p یه پارسر ِه. دقیقتر بگیم، یه پارسر ِ کاراکتره. توابع one و oneTwo تایپشون Parser Char هست. خودتون میتونین تایپِ one' و oneTwo' رو چک کنین.
به خاطرِ ابهامی که پیش میومد، لازم بود تایپِ testParse رو تعیین کنیم تا بشه چیزی که پارس کردیم رو نشون بدیم (Show کنیم).
یکی از نکات کلیدی اینه که داریم با پارسرها مثلِ مقادیر برخورد میکنیم و ترکیب ِ اونها رو با توابعِ معمولی یا عملگرهای Applicative و Monad انجام میدیم. اینجا ساختار ِ Applicative یا Monad، خودِ Parser ِه.
بعد یه تابع مینویسیم که یه نوشته رو با یه پیشوندِ خطجدید (\n) به خروجیِ استاندارد (یا stdout) چاپ کنه، و اون تابع رو به عنوانِ بخشی از یه main که نشون میده تا نقطهای که رسیده چی بدست آورده استفاده میکنیم.
pNL s =
putStrLn ('\n' : s)
main = do
pNL "stop:"
testParse stop
pNL "one:"
testParse one
pNL "one':"
testParse one'
pNL "oneTwo:"
testParse oneTwo
pNL "oneTwo':"
testParse oneTwo'اجراش کنیم و نتیجههاش رو ببینیم. چون اینها برگ چای نیستن و بجاش حروفِ روی صفحهی کامپیوتراند، اسمش رو میذاریم کار علمی. اگه هنوز به ارزش علمی این کار متقاعد نشدین، اجازه دارین یه کت آزمایشگاهی بپوشین و خروجی رو با یه چاپگر ماتریس سوزنی چاپ کنین. بعضی از شما شاید اصلاً ندونین یه چاپگر ماتریس سوزنی چی هست.*
مشت به آسمون...
main رو اجرا کنین و ببینین چی میشه:
Prelude> main
stop:
Failure (interactive):1:1: error: unexpected
stop
123<EOF>
^آناً بدون اینکه هیچ ورودیای مصرف کنه شکست خورد، به همین خاطر اون نشانگر در پیغامِ خطا، اولِ نوشتهست.
جواب بعدی:
one:
Success '1'یه کاراکتر مجرد رو پارس کردیم، رقمِ ۱. اینکه میدونیم موفق شدیم، جوابه. اما مابقیِ جریان ِ ورودی چطور؟ خوب اون چیزی که باهاش پارسر رو اجرا کردیم، باقیِ ورودی رو انداخت زمین. راههایی برای تغییر چنین رفتاری وجود دارن که تو تمرینها توضیح میدیم.
بعدی:
one':
Failure (interactive):1:2: error: unexpected
stop
123<EOF>
^یک کاراکتر رو با موفقیت پارس کردیم، ولی چون از >> برای سکانس کردن با stop اسفاده کرده بودیم، مقدارِ پارسشده رو انداختیم. یعنی به خاطر موفقیتِ پارسر ِ قبلی، نشانگر یه کاراکتر رفته جلو. trifecta میگه پارسر کجا شکست خورد، که خیلی کمک میکنه.
و جواب آخر:
oneTwo:
Success '2'
oneTwo':
Failure (interactive):1:3: error: unexpected
stop
123<EOF>
^مشابه قبلیهاست، فقط دو کاراکتر رو مستقل از هم پارس کردیم. اگه نخوایم اولین کاراکتری که پارس کردیم رو دور بندازیم، و در عوض "12" رو پارس کنیم چطور؟ تمرینِ زیر!
تمرینها: تمرینِ پارسینگ
۱.
یه ترکیبکننده وجود داره که میشه باهاش تعیین کرد در یک جای معین از جریان ورودی، انتظارِ پایانِ جریان رو داشته باشه. در کتابخونه ِ parsers اسمِ این تابع eof ِه (مخففِ end-of-file به معنای پایانِ فایل) و در ماژول ِ Text.Parser.Combinators قرار داره. ببینین میتونین کاری کنین پارسرهای one و oneTwo به خاطرِ نرسیدن به پایانِ جریان شکست بخورن!
۲.
با استفاده از string یه Parser بسازین که از ورودیِ نمونه (در مثالهای بالا) به ترتیب "1"، "12"، و "123" رو پارس کنه. سعی کنین با stop هم ترکیب ِش کنین. یعنی یک دونه پارسر باید بتونه هر سهتای اون نوشتهها رو پارس کنه.
۳.
سعی کنین با استفاده از char یه Parser بنویسین که همون کارِ string رو انجام میده.
آنتراکت: پارس ِ بیشتر
بیاین با این پارسرها بازی کنیم! معمولاً برای اجرای پارسرها از تابعِ parseString استفاده میکنیم، اما اگه راهی پیدا کردین که باهاش راحتتر بودین، ازش استفاده کنین! یه ذره پارسینگ نوشتیم تا یه کم بیشتر با قضیه آشنا بشین:
λ> import Text.Trifecta
λ> :t char
char :: CharParsing m => Char -> m Char
λ> :t parseString
parseString
:: Parser a
-> Text.Trifecta.Delta.Delta
-> String
-> Result a
λ> let gimmeA = char 'a'
λ> :t parseString gimmeA mempty
parseString gimmeA mempty :: String -> Result Char
λ> parseString gimmeA mempty "a"
Success 'a'
λ> parseString gimmeA mempty "b"
Failure (interactive):1:1: error: expected: "a"
b<EOF>
^
λ> parseString (char 'b') mempty "b"
Success 'b'
λ> parseString (char 'b' >> char 'c') mempty "b"
Failure (interactive):1:2: error: unexpected
EOF, expected: "c"
b<EOF>
^
λ> parseString (char 'b' >> char 'c') mempty "bc"
Success 'c'
λ> parseString (char 'b' >> char 'c') mempty "abc"
Failure (interactive):1:1: error: expected "b"
abc<EOF>
^به نظر میرسه بجای اینکه مجبور باشیم بیت به بیت پارسِرهای کاراکتر رو پشت هم سوار کنیم. باید یه راهی برای اینکه بگیم "این نوشته رو پارس کن" وجود داشته باشه، اینطور نیست؟ از قرار معلوم، وجود داره:
λ> parseString (string "abc") mempty "abc"
Success "abc"
λ> parseString (string "abc") mempty "bc"
Failure (interactive):1:1: error: expected: "abc"
bc<EOF>
^
λ> parseString (string "abc") mempty "ab"
Failure (interactive):1:1: error: expected: "abc"
ab<EOF>
^یک پارسر به تنهایی قرار نیست تمام ورودیش رو مصرف کنه – فقط مقداری از نوشته رو که برای تولید یه مقدار از تایپِ درخواستی لازم داره مصرف میکنه.
λ> parseString (char 'a') mempty "abcdef"
Success 'a'
λ> let stop = unexpected "stop pls"
λ> parseString (char 'a' >> stop) mempty "abcdef"
Failure (interactive):1:2: error: unexpected
stop pls
abcdef<EOF>
^
λ> parseString (string "abc") mempty "abcdef"
Success "abc"
λ> parseString (string "abc" >> stop) mempty "abcde"
Failure (interactive):1:4: error: unexpected
stop pls
abcde<EOF>
^با trifecta، مقادیرِ ByteString که با UTF-8 کدبندی شدن هم میشه پارس کرد:
λ> import Text.Trifecta
λ> :t parseByteString
parseByteString
:: Parser a
-> Text.Trifecta.Delta.Delta
-> Data.ByteString.Internal.ByteString
-> Result a
λ> parseByteString (char 'a') mempty "a"
Success 'a'بازی دیگه بسه. برگردیم سراغ چیزای جدی.