۱۳ - ۸گرامر 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
بذاریم و همونطوری کار میکرد، اما اینجوری شلوغ پلوغ میشه، نینجاهای هسکل هم میان سراغتون تا ازتون ناامید بشن.