۲۸ - ۷فانکتور، اپلیکِتیو، و مونَدِ IO

اشتباه دیگه‌ای که مردم می‌کنن اینه که طوری القا می‌کنن که ‏‎IO‎‏ یه ‏‎Monad‎‏ ِه؛ بجای اینکه بگن مثلِ همه‌ی ‏‎Monad‎‏‌ها، ‏‎IO‎‏ هم یه نوع‌داده هست که یک نمونه ِ ‏‎Monad‎‏ داره (‏‎Functor‎‏ و ‏‎Applicative‎‏ هم داره):

‏‎fmap‎‏: اجراییه ای بساز که همون اثرات رو اجرا می‌کنه، فقط ‏‎a‎‏ رو به ‏‎b‎‏ تغییر میده:

fmap :: (a -> b) -> IO a -> IO b

‏‎(<*>)‎‏: اجراییه ای بساز که اثراتِ هر دو آرگومان (تابع و مقدار) رو اجرا می‌کنه، و تابع رو به مقدار اعمال می‌کنه:

(<*>) :: IO (a -> b) -> IO a -> IO b

‏‎join‎‏: اثراتِ یک اجراییه ِ ‏‎IO‎‏ ِ تودرتو رو تلفیق کن:

join :: IO (IO a) -> IO a

‏‎IO Functor‎‏

‏‎fmap‎‏ نسبت به ‏‎IO‎‏ چه معنایی میده؟ طبق معمول میریم سراغ مثال:

fmap (+1) (randomRIO :: IO Int)

برای رسیدن به اون مقدارِ ‏‎Int‎‏ باید چندتا اثر اجرا کنیم. کاری که ‏‎fmap‎‏ اینجا انجام میده اینه که اون تابعِ افزاینده رو از روی اثراتی که برای بدست آوردنِ مقدارِ ‏‎Int‎‏ ممکنه اجرا کنیم لیفت می‌کنه، چون اینجا اثرات جزئی از اون ساختار ِ ‏‎IO‎‏ هستن. بیانیه‌ی بالا یه دستورالعملی برای بدست آوردنِ یه ‏‎Int‎‏ برمی‌گردونه، که یکی هم به نتیجه‌ی حاصل از اجراییه ِ اصلی که تابع از روش لیفت شده بود اضافه می‌کنه.

نکته اینجاست که اینجا هیچ اثری اجرا نکردیم. فقط با تغییرِ نتیجه‌ی نهاییِ یه اجراییه ‏‎IO‎‏، یه اجراییه ِ ‏‎IO‎‏ ِ جدید بر مبنای اجراییه ِ قبلی درست کردیم.

‏‎Applicative‎‏ و ‏‎IO‎‏

همونطور که در فصلِ اپلیکتیو هم گفتیم، ‏‎IO‎‏ یه نمونه ِ ‏‎Applicative‎‏ هم داره. شاید مثالی شبیهِ این یادتون باشه:

Prelude> (++) <$> getLine <*> getLine
hello
julie
"hellojulie"

اینجا اوپراتورِ الحاق رو روی دوتا ‏‎IO String‎‏ ِ احتمالی ‏‎fmap‎‏ کردیم تا نتیجه‌ی نهایی رو درست کنیم. یه مثالِ جذاب‌تر ببینیم:

    (+)
<$> (randomIO :: IO Int) 
<*> (randomIO :: IO Int)

بعد از اون ‏‎fmap‎‏ ِ اول، راهی برای بدست آوردنِ تابعی داریم که بصورتِ مانویدی از روی راهی برای بدست آوردنِ یه ‏‎Int‎‏ لیفت شده. یعنی نهایتاً میرسیم به یک راهی برای بدست آوردنِ نتیجه‌ی اعمال ِ تابع، که اثراتِ هر دو رو اجرا می‌کنه.

‏‎Monad‎‏ و ‏‎IO‎‏

برای ‏‎IO‎‏، ‏‎pure‎‏ یا ‏‎return‎‏ رو میشه به عنوانِ پوشوندنِ بی‌اثر ِ یک مقدار، در یک محیط ِ دستورالعمل-ساز دونست. مثال‌های زیر رو در نظر بگیریم.

GHCi اساساً دو کار انجام میده: می‌تونه مقادیری که توی ‏‎IO‎‏ نیستن رو چاپ کنه، مثل اینها:

Prelude> "I'll pile on the candy"
"I'll pile on the candy"
Prelude> 1
1

و می‌تونه اجراییه‌های ‏‎IO‎‏ رو اجرا، و در صورتِ وجود، جواب‌شون رو چاپ کنه. وقتی مقداری با تایپِ ‏‎IO (IO a)‎‏ دارین، در واقع دستورالعملی برای درست کردنِ دستورالعملی دارین که یه ‏‎a‎‏ تولید می‌کنه. ببینید چرا ‏‎print‎‏ در مثال زیر چیزی چاپ نمی‌کنه:

Prelude> let embedInIO = return :: a -> IO a
Prelude> embedInIO 1
1
Prelude> :{
*Main| let s =
*Main|       "I'll put in some ingredients"
*Main| :}
Prelude> embedInIO (print s)

برای اینکه اون اثرات رو با هم تلفیق کنیم تا یک ‏‎IO a‎‏ که نتیجه رو توی GHCi چاپ می‌کنه بگیریم، ‏‎join‎‏ لازم داریم:

Prelude> let s = "It's a piece of cake"
Prelude> join $ embenInIO (print s)
"It's a piece of cake"
Prelude> embedInIO (embedInIO 1)
Prelude> join $ embedInIO (embedInIO 1)
1

چیزی که ‏‎IO Monad‎‏ رو از ‏‎Applicative‎‏ متمایز می‌کنه اینه که اثراتی که توسطِ ‏‎IO‎‏ ِ بیرونی اجرا میشن ممکنه به اینکه چه دستورالعملی در بخشِ داخلی می‌گیرین تأثیر بذارن. با تودرتویی هم میشه وابستگی به ترتیب‌بندی رو بیان کرد، که همونطور که پیتر جِی. لندین هم اشاره کرده، از کلک‌های مفید در جبرهای لاندا ِه.*

*

‏‎A correspondence between ALGOL 60 and Church’s Lambda-notations; P. J. Landin‎‏

یه مثال از اثر:

module NestedIO where

import Data.Time.Calendar
import Data.Time.Clock
import System.Random

huehue :: IO (Either (IO Int) (IO ()))
huehue = do
  t <- getCurrentTime
  let (_, _, dayOfMonth) =
        toGregorian (utctDay t)
  case even dayOfMonth of
    True ->
      return $ Left randomIO
    False ->
      return $ Right (putStrLn "no soup for you")

اجراییه ِ ‏‎IO‎‏ یی که این تابع برمی‌گردونه، بستگی به اثراتی که برای دیدنِ روزِ ماه اجرا می‌کنه داره (که ببینه روزِ ماه زوج‌ه یا فرد).* دقت کنین که چنین چیزی قابل بیان با ‏‎Applicative‎‏ نیست. اگه یه راه ساده برای اجرا و تستِ این تابع می‌خواین، کدِ زیر رو امتحان کنین:

Prelude> blah <- huehue
Prelude> either (>>= print) id blah
-7077132465932290066

ما این رو ۲۸ ژانویه نوشتیم. مالِ شما ممکنه فرق کنه.

*

چرا؟ خب فصلِ موند خیلی وقت پیش بود، باید یه جوری مرموز بشیم.

شرکت‌پذیری ِ موندی

وقتی به هسکل‌نویس‌ها میگن بایند ِ ‏‎Monad‎‏ شرکت‌پذیر ِه، اکثراً گیج میشن، چون ‏‎IO‎‏ رو به چشمِ یه مثال نقض می‌بینن. اشتباه‌شون اینجاست که ساخت ِ اجراییه‌های ‏‎IO‎‏ رو، با اجرای اونها اشتباه می‌گیرن. تا وقتی با هسکل کار می‌کنیم، اجراییه‌های ‏‎IO‎‏ رو فقط برای اجرا شدن توسطِ ‏‎main‎‏ می‌سازیم. از لحاظ مفهومی، اجراییه‌های ‏‎IO‎‏ کاری نیستن که انجام میدیم، چیزی‌اند که راجع بهشون حرف می‌زنیم. بایند از روی یه اجراییه ِ ‏‎IO‎‏، اجراش نمی‌کنه، فقط برمبنای اون اجراییه ِ ‏‎IO‎‏ یه اجراییه ِ ‏‎IO‎‏ ِ جدید درست می‌کنه.

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