۱۶ - ۱۴فانکتورِ IO یا IO Functor

تایپِ ‏‎IO‎‏ رو قبلاً در فصل‌های ساختِ پروژه و تستینگ دیدیم، ولی به غیر از چاپ ِ نوشته و گرفتنِ ورودی از کاربر کارِ زیادی باهاش نمی‌کردیم. جلوتر در کتاب یه فصلِ کامل برای ‏‎IO‎‏ هست. این تایپ، یه نوع‌داده ِ انتزاعی ِه؛ هیچ داده‌سازی که بشه روش تطبیق الگو کرد نداره، پس تنها راهی که میشه با مقادیرِ تایپِ ‏‎IO a‎‏ تعامل کرد، از طریقِ تایپکلاس‌هایی‌ه که ارائه میده. یکی از ساده‌ترین‌هاشون ‏‎Funtor‎‏ ِه:

-- getLine :: IO String
-- read :: Read a => String -> a

getInt :: IO Int 
getInt = fmap read getLine

تایپِ ‏‎Int‎‏ یه نمونه ِ ‏‎Read‎‏ داره، و ‏‎fmap‎‏ تابعِ ‏‎read‎‏ رو از روی تایپِ ‏‎IO‎‏ لیفت می‌کنه. میشه گفت ‏‎getLine‎‏ یک راه برای بدست آوردنِ یه نوشته هست، خودش ‏‎String‎‏ نیست. معنیِ ‏‎IO‎‏ اینه که ممکنه با اثر باشه، ولی لزوماً اینطور نیست. اینجا عوارض جانبی لازمه تا بلوکه کنه و منتظرِ ورودیِ کاربر از طریقِ جریانِ ورودیِ استاندارد ای که سیستم عامل تأمین می‌کنه باشه:

Prelude> getInt
10
10

نوشتیم ۱۰ و enter زدیم. GHCi مقادیرِ ‏‎IO‎‏ رو چاپ می‌کنه، مگر اینکه تایپ‌شون ‏‎IO ()‎‏ باشه (که مقدارِ واحد رو مخفی می‌کنه چون بی‌معنی‌ه):

Prelude> fmap (const ()) getInt
10

اون ۱۰ که بالا نوشته شده، ۱۰ ایه که ما تایپ کردیم و enter زدیم. GHCi بعد از اون چیزی رو چاپ نمی‌کنه چون اون مقدارِ ‏‎Int‎‏ که از ‏‎String‎‏ گرفتیم رو با ‏‎()‎‏ عوض کردیم (از طریق اعمال ِ ‏‎const ()‎‏ به محتویاتِ ‏‎IO Int‎‏). اگه حضورِ ‏‎IO‎‏ رو نادیده بگیریم، مثل این میشه:

Prelude> let getInt = 10 :: Int
Prelude> const () getInt
()

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

Prelude> return 1 :: IO Int
1
Prelude> ()
()
Prelude> return () :: IO ()
Prelude>

اگه کار مفیدتری بخوایم انجام بدیم چطور؟ میشه هر تابعی رو با ‏‎fmap‎‏ از روی ‏‎IO‎‏ لیفت کنیم:

Prelude> fmap (+1) getInt
10
11

Prelude> fmap (++ " and me too!") getLine
hello
"hello and me too!"

همون کاری که با ‏‎Functor‎‏ انجام دادیم رو با گرامر ِ ‏‎do‎‏ هم میشه انجام داد:

meTooIsm :: IO String
meTooIsm = do
  input <- getLine
  return (input ++ "and me too!")

bumpIt :: IO Int
bumpIt = do
  intVal <- getInt
  return (intVal + 1)

اگه ‏‎fmap f‎‏ برای کاری که می‌خواین انجام بدین کفایت می‌کنه، معمولاً اونطوری کوتاه‌تر و واضح‌تر میشه. این که اول کُدتون رو با نسخه‌ی "وراج‌تر" یا طولانی‌ترِ بعضی بیانیه شروع کنین و بعد از اینکه کار کرد خلاصه‌ش کنین، روندِ خیلی رایجی‌ه و هیچ ایرادی نداره.