۲۸ - ۴اشتراکگذاری
علاوه بر اجبارِ ترتیببندی، IO خیلی از اشتراکگذاریهایی که در فصلِ نااکیدی گفتیم رو خاموش میکنه. همونطور که به زودی میبینیم، همهی حالتهای اشتراکگذاری رو لغو نمیکنه – اصلاً نمیتونه، چون همهی برنامههای هسکل یه اجراییه ِ main با تایپِ (الزاماً) IO دارن. یه کم جلوتر میرسیم.
فعلاً ببینیم چه اشتراکگذاریهایی لغو میشن و چرا. توی هسکل معمولاً میشه با اطمینانِ کامل گفت که اگه یه تابع قرار باشه حساب بشه، خروجیش مقداری از تایپِ بخصوصی میشه (که ممکنه مقدارِ Nothing یا لیست خالی هم باشه). وقتی تایپ رو تعیین میکنیم، یعنی میگیم: "اگه یه موقع این تابع حساب بشه، نتیجهش مقداری از این تایپ میشه."
اما با تایپِ IO هیچ تضمینی در کار نیست. مقادیرِ تایپِ IO a یه a نیستن؛ در واقع توصیفی از نحوهی بدست آوردنِ احتمالیِ یه a هستن. یه چیزی با تایپِ IO String، محاسبهای نیست که در صورتِ محاسبه یه String خروجی بده؛ توصیفی از اینه که چطور ممکنه از "دنیای واقعی" اون String رو بگیرین، که احتمالاً در طول مسیر هم اثراتی اجرا میکنه. توصیفِ اجراییههای IO اونها رو اجرا نمیکنه، درست همونطور که دستورِپختِ کیک بهخودیِخود کیک نمیده.*
توضیحی که برنت یورگی برای کلاسِ cis194 در UPenn داده رو نگاه کنین.
در این محیط که مقدار ندارین، و در واقع فقط راهِ گرفتن یه مقدار رو دارین، مفهومی نداره بگیم میشه اون مقدار رو به اشتراک گذاشت.
وقتشه
پس یکی از مشخصاتِ کلیدیِ IO اینه که اشتراکگذاری رو خاموش میکنه. در ماژول ِ Data.Time.Clock، تابعی هست که از ساعتِ سیستم، زمانِ لحظهایِ UTC (ساعتِ هماهنگ جهانی) رو میگیره:
getCurrentTime :: IO UTCTimeاگه IO جلوی اشتراکگذاری رو نمیگرفت، این چطور کار میکرد؟ یک بار که ساعت رو میگرفتین، نتیجهش به اشتراک گذاشته میشد و ساعت همون ساعتی که بارِ اول اجبارش کردین میموند. متأسفانه اینطوری نمیشه زمان رو نگه داشت، اما برنامهتون اصلاً اونطوری که قصد داشتین کار نمیکنه.
با اینکه یه مقدارِ اسمداره، یعنی میشه به اشتراک گذاشته بشه، پس چرا نمیشه؟
getCurrentTime :: IO UTCTime
-- ^-- اینیادتون باشه: چیزی که اینجا داریم فقط یه توصیف از نحوهی گرفتنِ زمانِ لحظهایه. هنوز زمانِ حالِ حاضر رو نداریم، پس مقداری نیست که بشه به اشتراک گذاشته بشه. خودمون هم نمیخوایم به اشتراک گذاشته بشه، چون میخوایم هر دفعه که اجراش میکنیم یه زمانِ جدید بده.
برای اجراش، main رو توی ماژول تعریف میکنیم تا سیستمِ زمانِ اجرا، پیدا و اجراش کنه. همه چیز داخلِ main، تایپِ IO داره تا همه چیز همونطور که انتظار دارین تودرتو و متسلسل اجرا بشن.
یک مثال دیگه
یه مثال دیگه از خاموش شدنِ اشتراکگذاری توسطِ IO ببینیم. حتماً تابعهای whnf و nf از criterion که فصلِ قبل استفاده کردیم خاطرِتون هستن. شاید یادتون باشه که همیشه میخوایم اشتراکگذاری برای اونها لغو بشه تا هر دفعه محاسبه بشن؛ اگه نتیجهشون به اشتراک گذاشته بشه، بجای اینکه بنچمارکینگ میانگینِ چندین بار محاسبه رو بده، فقط زمانِ اولین محاسبه رو میده. برای لغوِ اشتراکگذاری، اون تابعها رو به آرگومانها اعمال میکردیم.
اما مُدلِ IO ِ اونها نیازی به این اعمالِ تابع برای لغوِ اشتراکگذاری نداره، چون پارامترِ IO خودش اشتراکگذاری رو لغو میکنه. تایپهای زیر رو با هم مقایسه کنین:
whnf :: (a -> b) -> a -> Benchmarkable
nf :: NFData b
=> (a -> b) -> a -> Benchmarkable
whnfIO :: IO a -> Benchmarkable
nfIO :: NFData a => IO a -> Benchmarkableتوی این نسخههای IO دیگه نیازی به آرگومانِ تابعی نیست تا جلوی اشتراکگذاری گرفته بشه، چون همینکه IO میگیرن، اشتراکگذاری لغو میشه – دیگه بدونِ توسل به آرگومانِ اضافه، میشه چندین بار اجرا بشن.
همونطور که قبلاً هم گفتیم، IO همهی اشتراکگذاریها رو همهجا لغو نمیکنه؛ اونطوری، به خاطر اینکه main همیشه IO ِه، اشتراکگذاری بیمعنی میشد. ولی درک اینکه چرا و چه زمانی اشتراکگذاری لغو میشه مهمه، چون اگه درست متوجه نباشین، منجر میشه به اینکه...
کُد کار نمیکنه!
اینجا از یه مثال با استفاده از تایپِ MVar میزنیم. این براساسِ یه کدِ واقعیه که بالاخره IO رو به کریس یاد داد، و اولین مثالیه که کریس برای توضیحِ IO به جولی نشون داد.
با تایپِ MVar میشه دادههای به اشتراک گذاشته شده در هسکل رو هماهنگ کرد. خیلی اجمالی بخوایم بگیم، MVar در هر لحظه میتونه فقط یک مقدار توی خودش نگه داره. یه مقدار میذارین توش؛ انقدر نگهش میداره تا اون مقدار رو بیارین بیرون. فقط و فقط اون موقعست که میشه یه گربهی دیگه بذارین تو جعبه. ما اصلاً نمیتونیم بهتر از سایمون مارلو این مبحث رو توضیح بدیم، پس اگه اطلاعاتِ بیشتر میخواین، به شدت کتابِ مارلو رو پیشنهاد میکنیم.
خب، با این مثال میخوایم اول یه مقدار بذاریم توی یه MVar و بعد بیاریمش بیرون:
module WhatHappens where
import Control.Concurrent
myData :: IO (MVar Int)
myData = newEmptyMVar
main :: IO ()
main = do
mv <- myData
putMVar mv 0
mv' <- myData
zero <- takeMVar mv'
print zeroاین کُد خطا میده که به بنبست خورده. مشکل اینجاست که newEmptyMVar با تایپِ IO (MVar a) یه دستورالعمل برای درست کردنِ تعدادِ نامحدودی MVar ِ خالیه؛ ارجاع به یک مقدارِ بهاشتراکگذاشتهشدهی MVar نیست. به عبارتِ دیگه، اون دوتا myData به یک MVar اشاره نمیکنن.
گرفتن از یه MVar ِ خالی، بلوکه میشه تا یه چیزی تویِ MVar گذاشته بشه. ترتیبِ زیر رو فرض کنین:
take
put
take
putاین به درستی خاتمه پیدا میکنه. اول تلاش برای گرفتنِ یه مقدار از MVar، بلوکه شد، بعد یه مقدار توش گذاشته شد، بعد یه take ِ بلوکشدهی دیگه رخ داد، بعد هم یه put ِ دیگه برای ارضای take ِ دوم. خوبه و مشکلی نداره.
این مثال به بنبست میخوره:
put
take
takeهربخشی از برنامه که اون take ِ دوم رو اجرا کنه، بلوکه میشه تا یه put ِ دوم رخ بده. اگه برنامه طوری طراحی شده باشه که هیچ put ِ دیگهای در کار نباشه، به بنبست میخوره. یه خطای بنبست، مثلِ اینه:
Prelude> main
*** Exception:
thread blocked indefinitely
in an MVar operationوقتی تایپی مثلِ IO String دارین، یه String ندارین؛ راهی دارین که شاید بتونین از طریقش یه String بگیرین، که احتمالاً در طولِ این مسیر، اثراتی هم اجرا میشن. بطور مشابه، اتفاقی که با اون دوتا MVar (که دوتا زندگیِ متفاوت از هم داشتن) افتاد شبیه این بود:
mv mv'
put take (اون آخری)بطور خلاصه، این تایپ:
IO (MVar a)میگه دستورالعملی برای درستکردنِ تعدادِ نامحدودی MVarهای خالی دارین، نه اینکه ارجاعی به یک MVar ِ بهاشتراکگذاشته باشه.
MVar هم میشه به اشتراک گذاشت، اما باید صراحتاً انجام بشه تا ضمنی. اگه بعد از یک بار انقیاد، ارجاع ِ MVar به صراحت به اشتراک گذاشته نشه، همینطور MVar ِ جدید و خالی میده بیرون. باز هم پیشنهاد میکنیم هر وقت برای آشناییِ بیشتر با MVarها آماده بودین، برین سراغ کتابِ سایمون مارلو.