۲۳ - ۸پارسرهای کاراکتر و توکن

خیلی خوب، یه عالمه کُد بود، یه نفسی تازه کنیم.

احتمالاً تا الان یه حدسی از مفهومِ نشانه‌گذاری میزنین، ولی وقتش رسیده بریم سراغ جزئیات. نشانه‌گذاری یه تاکتیکِ کاربردی در پارسینگ ِه، به همین خاطر در ذاتِ بعضی از توابعِ کتابخونه‌ها که تا اینجا استفاده کردیم تعبیه شده. اینجا می‌خوایم بررسی‌ش کنیم.

قبلاً پارسینگ در دو مرحله انجام میشده، تحلیلِ واژگانی و تحلیلِ نحوی. اول کاراکترهای یک جریان به لِکسر داده میشدن، و لکسر بنا به درخواستِ پارسر، انقدر توکن بهش می‌داده تا تموم میشدن.* بعد پارسر جریان ِ توکن‌ها رو در یه درخت ساختاربندی می‌کرده به اسمِ درخت گرامری انتزاعی یا AST:

-- Stream یه جور تایپ‌های فرضی. از تایپ
-- استفاده کردیم چون پارسرهایی که برای
-- ‎‏محصولات نهایی در هسکل استفاده میشن،‏‎
-- [] به خاطر عملکرد بهتر، از
-- ‎‏استفاده نمی‌کنن.‏‎
lexer :: Stream Char -> Stream Token
parser :: Stream Token -> AST
*

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

لکسرها ساده‌تراند، معمولاً پارس‌هایی رو انجام میدن که نیازی به دیدنِ جلوتر در جریان، بیشتر از یک کاراکتر یا توکن نیست. بعضی اوقات به لکسرها، نشانه‌گذار گفته میشه. گاهی وقتها تحلیلِ واژگانی با عباراتِ باقاعده انجام میشه، اما در یه کتابخونه ِ پارسینگ ِ هسکل، معمولاً لکسینگ و پارسینگ هردو با یک API انجام میشن. لکسرها (یا نشانه‌گذارها) و پارسرها نقاطِ مشترک زیادی دارن، تفاوت‌های اصلی‌شون یکی در کاربردشون‌ه، و یکی در کلاسِ گرامری‌شون.*

*

گرامرهای صوری (قواعدِ ایجاد ِ نوشته در یه زبانِ صوری) معمولاً در یه سلسله مراتب به نامِ سلسله مراتب چامسکی قرار داده میشن (نام‌گذاری شده به نامِ زبان‌شناس، نوآم چامسکی).

بازی با توکن‌ها

با چندتا چیز بازی کنیم تا نقش نشانه‌گذاری رو ببینیم:

λ> parseString (some digit) mempty "123 456"
Success "123"
λ> parseString (some (some digit)) mempty "123 456"
Success ["123"]

λ> parseString (some integer) mempty "123"
Success [123]
λ> parseString (some integer) mempty "123456"
Success [123456]

اینجا اگه می‌خواستیم ۱۲۳ و ۴۵۶ رو نوشته‌های مستقل بدونیم، باید از یه جور جداکننده استفاده می‌کردیم. میشه دستی این کار رو بکنیم، اما نشانه‌گذارها در ‏‎parsers‎‏ این کار رو خودشون انجام میدن، تازه انواع فاصله‌های سفید رو پوشش میدن:

λ> parseString (some integer) mempty "123 456"
Success [123,456]
λ> parseString (some integer) mempty "123\n\n  456"
Success [123,456]

یا حتی ترکیبی از فاصله و خطِ‌جدید رو:

λ> parseString (some integer) mempty "123 \n \n 456"
Success [123,456]

اما اگه همینطوری ‏‎token‎‏ رو به ‏‎digit‎‏ اعمال کنین، کاری که انتظار دارین رو انجام نمیده:

λ> let s = "123 \n \n 456"
λ> parseString (token (some digit)) mempty s
Success "123"
λ> parseString (token (some (token digit))) mempty s
Success "123456"

λ> parseString (some decimal) mempty s
Success [123]
λ> parseString (some (token decimal)) mempty s
Success [123,456]

با تابعِ ‏‎integer‎‏ که خودش یه نشانه‌گذار هست مقایسه کنین:

λ> parseString (some integer) mempty "1\n2\n 3\n"
Success [1,2,3]

میشه یه پارسرِ نشانه‌گذار مثلِ ‏‎some integer‎‏ اینطوری بنویسیم:

p' :: Parser [Integer]
p' = some $ do
  i <- token (some digit)
  return (read i)

می‌تونیم خروجی‌ش رو با خروجیِ حاصل از اعمال ِ ‏‎token‎‏ به ‏‎digit‎‏ مقایسه کنیم:

λ> let s = "1\n2\n3"
λ> parseString p' mempty s
Success [1,2,3]

λ> parseString (token (some digit)) mempty s
Success "1"
λ> parseString (some (token (some digit))) mempty s
Success ["1","2","3"]

باید به گستره‌ای که توش نشانه‌گذاری می‌کنین خوب فکر کنین:

λ> let tknWhole = token $ char 'a' >> char 'b'
λ> parseString tknWhole mempty "a b"
Failure (interactive):1:2: error: expected: "b"
a b<EOF>
 ^
λ> parseString tknWhole mempty "ab ab"
Success 'b'
λ> parseString (some tknWhole) mempty "ab ab"
Success "bb"

اگه می‌خواستیم مثال اول کار کنه، باید پارس ِ کاراکتر اول رو نشانه‌گذاری می‌کردیم، نه کلِ پارس ِ ‏‎a‎‏-بعد-‏‎b‎‏ رو:

λ> let tknCharA = (token (char 'a')) >> char 'b'
λ> parseString tknCharA mempty "a b"
Success 'b'
λ> parseString (some tknCharA) mempty "a ba b"
Success "bb"
λ> parseString (some tknCharA) mempty "a b a b"
Success "b"

مثال آخر بعد از پارس ِ اولین ‏‎a b‎‏ متوقف میشه، چون پارسر چیزی از یه فاصله بعد از ‏‎b‎‏ نمیگه، و "رفتار" نشانه‌گذاری هم فقط به چیزی که بعد از ‏‎a‎‏ اومده اعمال میشه. می‌تونیم هر دو پارسرِ کاراکتر رو نشانه‌گذاری کنیم:

> let tknBoth = token (char 'a') >> token (char 'b')
> parseString (some tknBoth) mempty "a b a b"
uccess "bb"

یه هشدار ملایم: خیلی برای نشانه‌گذاری ذوق نکنین. استفاده‌ی زیادی از پارسرهایی که نشانه‌گذاری می‌کنن، یا ترکیب اونها با پارسرهای کاراکتر ممکنه پارسر‌ِتون رو کُند، یا درک‌ش رو سخت کنه. از قضاوت خودتون استفاده کنین. به خاطر داشته باشین که نشانه‌گذاری فقط مرتبط با فاصله‌های سفید نیست؛ برای نادیده گرفتنِ شلوغی‌هاست تا بشه روی ساختارهایی که پارس می‌کنین تمرکز کنین.