۲۸ - ۷فانکتور، اپلیکِتیو، و مونَدِ IO
اشتباه دیگهای که مردم میکنن اینه که طوری القا میکنن که IO یه Monad ِه؛ بجای اینکه بگن مثلِ همهی Monadها، IO هم یه نوعداده هست که یک نمونه ِ Monad داره (Functor و Applicative هم داره):
fmap: اجراییه ای بساز که همون اثرات رو اجرا میکنه، فقط a رو به b تغییر میده:
fmap :: (a -> b) -> IO a -> IO b(<*>): اجراییه ای بساز که اثراتِ هر دو آرگومان (تابع و مقدار) رو اجرا میکنه، و تابع رو به مقدار اعمال میکنه:
(<*>) :: IO (a -> b) -> IO a -> IO bjoin: اثراتِ یک اجراییه ِ IO ِ تودرتو رو تلفیق کن:
join :: IO (IO a) -> IO aIO 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 و دستورپخت رو به یاد بیارین، قیاسی که برنت یورگی معرفی کرده و ما هم خیلی دوست داریم.