۱۳ - ۸گرامر do و IO

بالاتر یه کم نوشتار ِ ‏‎do‎‏ رو توضیح دادیم، اما اینجا می‌خوایم چندتا چیزِ دیگه هم ازش بگیم. بلوکهای ‏‎do‎‏ در واقع شکرهای گرامری هستن که برای ساده‌سازیِ تسلسل ِ اجراییه‌ها استفاده میشن. اما چون چیزی بیشتر از شکر گرامری نیستن، اصولاً واجب نیستن. از مزایاشون اینه که با مخفی کردن تودرتویی ِ توابع، خواناییِ کُد رو بیشتر می‌کنن، و به کمک اونها میشه بدونِ درکِ مونَدها و ‏‎IO‎‏، کُدِ اثردار (اثر روی دنیای بیرون) نوشت. پس حسابی تو این فصل ازش استفاده می‌کنیم (مثل کُدهای واقعیِ هسکل).

در یه برنامه‌ی هسکل، ‏‎main‎‏ همیشه باید تایپِ ‏‎IO ()‎‏ داشته باشه. با گرامر ِ ‏‎do‎‏ میشه اجراییه‌های مونَدیک رو متسلسل کنیم. ‏‎Monad‎‏ یه تایپکلاس‌ه که با جزئیات در یکی از فصل‌های بعد توضیح میدیم؛ اما اینجا فقط با یکی از تایپ‌هایی که نمونه ِ مونَد داره کار داریم، که اون هم ‏‎IO‎‏ ِه. به همین خاطر، تابع‌های ‏‎main‎‏ اکثراً (نه همیشه) با بلوک ِ ‏‎do‎‏ نوشته میشن.

با این گرامر میشه مقادیرِ حاصل از اجراییه‌های موندیکِ ‏‎IO‎‏ رو نام‌گذاری کرد و به عنوان ورودی برای اجراییه‌های آتیِ برنامه به کار برد. یه مثال خیلی ساده از یه بلوک ِ ‏‎do‎‏ ببینیم تا یه درک اجمالی پیدا کنیم:

main = do -- [1]
    x1   <- getLine
-- [2]   [3]   [4]
    x2   <- getLine
--          [5]
    return (x1 ++ x2)
--   [6]      [7]

۱.

کلیدواژه ِ ‏‎do‎‏، بلوک ِ اجراییه‌های ‏‎IO‎‏ رو معرفی می‌کنه.

۲.

‏‎x1‎‏ یه متغیره که مقدارِ حاصل از اجراییه ِ ‏‎IO‎‏ (به اسمِ ‏‎getLine‎‏) رو نشون میده.

۳.

علامتِ ‏‎<-‎‏ متغیرِ سمت چپ رو به نتیجه‌ی اجراییه ِ ‏‎IO‎‏ در سمت راست انقیاد میده.

۴.

‏‎getLine‎‏ تایپ‌ش ‏‎IO String‎‏ ِه، و نوشته ِ ورودیِ کاربر رو می‌گیره. در این مثال، مقدارِ نوشته‌ای که کاربر وارد می‌کنه، همون مقداری میشه که به ‏‎x1‎‏ انقیاد داده میشه.

۵.

‏‎x2‎‏ یه متغیره که مقدارِ حاصل از ‏‎getLine‎‏ ِ دوم رو نشون میده. مثلِ بالا، انقیاد توسطِ ‏‎<-‎‏ انجام میشه.

۶.

‏‎return‎‏ رو به زودی با جزئیات بیشتر توضیح میدیم، اما اینجا اجراییه‌ی پایانی برای بلوک ِ ‏‎do‎‏ ِه.

۷.

این مقداری‌ه که ‏‎return‎‏ برمی‌گردونه – اتصالِ دو نوشته‌ای که از دو اجراییه ِ ‏‎getLine‎‏ بدست آوردیم.

درسته که ‏‎<-‎‏ برای انقیاد ِ یه متغیر به کار میره، اما با بقیه‌ی روش‌های نام‌گذاری و انقیاد ِ متغیرها که در فصل‌های قبل دیدم متفاوت‌ه. این فِلِش جزئی از شکر ِ ‏‎do‎‏ ِه، و یه اسم رو به ‏‎a‎‏ از یه ‏‎m a‎‏ (که ‏‎m‎‏ یه ساختارِ موندیک ِه؛ در مثال بالا میشه ‏‎IO‎‏) بایند می‌کنه. اون فِلِش ‏‎<-‎‏ این امکان رو میده که ‏‎a‎‏ رو از زیرِ ساختارش استخراج و نام‌گذاری کنیم، و در گستره ِ محدودِ بلوک ِ ‏‎do‎‏ ازش به عنوان ورودی به یه بیانیه‌ی دیگه استفاده کنیم. هر انقیاد با استفاده از ‏‎<-‎‏ یه متغیرِ موجود رو تغییر نمیده، بلکه یه متغیر جدید می‌سازه، چون داده‌ها تغییرناپذیر اند.

‏‎return‎‏

این تابع واقعاً کار زیادی انجام نمیده، اما با توجه به طرزِ کارِ موندها و ‏‎IO‎‏، نقشِ مهمی داره. کاری غیر از برگردوندن ِ یه مقدار انجام نمیده، اما اون مقدار رو داخل یه ساختارِ موندیک برمی‌گردونه:

Prelude> :t return
return :: Monad m => a -> m a

تو این فصل، ‏‎return‎‏ یه مقدار رو داخلِ ‏‎IO‎‏ برمی‌گردونه. به خاطرِ اینکه تایپ‌ِِ ‏‎main‎‏ باید ‏‎IO ()‎‏ باشه، پس مقدار آخر هم باید تایپِ ‏‎IO ()‎‏ داشته باشه، و با ‏‎return‎‏ میشه بدونِ اضافه کردنِ یه تابعِ اضافه، مقدار آخر رو در ‏‎IO‎‏ بذاریم. اگه آخرین کاری که یه بلوک ِ ‏‎do‎‏ انجام میده ‏‎return ()‎‏ باشه، معنی‌ش اینه که هیچ مقداری وجود نداره که بعد از انجام همه‌ی اجراییه‌های I/O برگردونه. اما چون برنامه‌های هسکل نمی‌تونن هیچ چیز برنگردونن، این توپل ِ خالی به اسمِ واحد رو فقط به خاطر اینکه یه چیزی برگردونده باشن، خروجی میدن. در REPL، توپل ِ خالی رو صفحه چاپ نمیشه، اما پشت پرده وجود داره.

حالا ‏‎return‎‏ رو در عمل نشون میدیم. فرض کنیم می‌خوایم دو حرف از کاربر بگیریم و تساوی‌شون رو بررسی کنیم. این کار رو نمیشه کرد:

twoo :: IO Bool
twoo = do c  <- getChar
          c' <- getChar
          c == c'

خودتون امتحان کنین و ببینین چه جور خطای تایپی می‌گیرین. بهتون میگه که تایپ مورد انتظار، تایپِ ‏‎IO Bool‎‏ بوده و با تایپِ واقعی از ‏‎c == c'‎‏ که ‏‎Bool‎‏ هست، جور نیست. پس خط آخر باید مقدارِ ‏‎Bool‎‏ رو داخلِ ‏‎IO‎‏ برگردونه:

twoo :: IO Bool
twoo = do c  <- getChar
          c' <- getChar
          return (c == c')

مقدارِ ‏‎Bool‎‏ رو توسطِ ‏‎return‎‏ داخلِ ‏‎IO‎‏ قرار دادیم. خیلی خوب. حالا برای مواردی که می‌خوایم هیچ چیز برنگردونیم چطور؟ از همون مثال بالا استفاده می‌کنیم، فقط یه ‏‎if-then-else‎‏ هم بهش اضافه می‌کنیم:

main :: IO ()
main = do c  <- getChar
          c' <- getChar
          if c == c'
           then putStrLn "True"
           else return ()

اگه دو تا حرف ِ ورودی مساوی باشن چه اتفاقی میوفته؟ اگه نباشن چطور؟

به قول بعضی‌ها، گرامر ِ ‏‎do‎‏ این حس رو میده که انگار داریم تو هسکل به سبکِ برنامه‌نویسیِ دستوری کُد می‌نویسیم. دقت داشته باشین که در این سبکِ دستوری و اثردار، واجبه در تایپِ خروجی، ‏‎IO‎‏ داشته باشیم. نمیشه بدونِ اینکه در تایپ مشخص شده باشه، اثر پیاده کنیم. ‏‎do‎‏ فقط یه شکر گرامری ِه، اما گرامرِ موندیک که در یکی از فصل‌های آتی توضیح میدیم، برای موندهای غیر از ‏‎IO‎‏ هم طرزِ کارِ مشابهی داره.

نوشتار ِ ‏‎do‎‏ مُضِره!

شوخی کردیم. اما گاهی هسکل‌نویس‌های پرشوق زیادی از بلوکهای ‏‎do‎‏ استفاده می‌کنن. استفاده از ‏‎do‎‏ برای بیانیه‌های تک‌خطی نه لازم‌ه، نه سبکِ خوبی به حساب میاد. بالاخره یاد می‌گیرین برای بیانیه‌های تک‌خطی از ‏‎>>=‎‏ بجای ‏‎do‎‏ استفاده کنین (تو این فصل یه مثال هست). به دلیل مشابه، استفاده از ‏‎do‎‏ با توابعی مثل ‏‎putStrLn‎‏ و ‏‎print‎‏ که در ذات اثردار هستن هم لازم نیست. در تابعِ بالا می‌تونستیم ‏‎do‎‏ رو پشت هر دو ‏‎putStrLn‎‏ و ‏‎return‎‏ بذاریم و همونطوری کار می‌کرد، اما اینجوری شلوغ پلوغ میشه، نینجاهای هسکل هم میان سراغ‌تون تا ازتون ناامید بشن.