۲۸ - ۴اشتراک‌گذاری

علاوه بر اجبارِ ترتیب‌بندی، ‏‎IO‎‏ خیلی از اشتراک‌گذاری‌هایی که در فصلِ نااکیدی گفتیم رو خاموش می‌کنه. همونطور که به زودی می‌بینیم، همه‌ی حالت‌های اشتراک‌گذاری رو لغو نمی‌کنه – اصلاً نمی‌تونه، چون همه‌ی برنامه‌های هسکل یه اجراییه ِ ‏‎main‎‏ با تایپِ (الزاماً) ‏‎IO‎‏ دارن. یه کم جلوتر می‌رسیم.

فعلاً ببینیم چه اشتراک‌گذاری‌هایی لغو میشن و چرا. توی هسکل معمولاً میشه با اطمینانِ کامل گفت که اگه یه تابع قرار باشه حساب بشه، خروجی‌ش مقداری از تایپِ بخصوصی میشه (که ممکنه مقدارِ ‏‎Nothing‎‏ یا لیست خالی هم باشه). وقتی تایپ رو تعیین می‌کنیم، یعنی میگیم: "اگه یه موقع این تابع حساب بشه، نتیجه‌ش مقداری از این تایپ میشه."

اما با تایپِ ‏‎IO‎‏ هیچ تضمینی در کار نیست. مقادیرِ تایپِ ‏‎IO a‎‏ یه ‏‎a‎‏ نیستن؛ در واقع توصیفی از نحوه‌ی بدست آوردنِ احتمالیِ یه ‏‎a‎‏ هستن. یه چیزی با تایپِ ‏‎IO String‎‏، محاسبه‌ای نیست که در صورتِ محاسبه یه ‏‎String‎‏ خروجی بده؛ توصیفی از اینه که چطور ممکنه از "دنیای واقعی" اون ‏‎String‎‏ رو بگیرین، که احتمالاً در طول مسیر هم اثراتی اجرا می‌کنه. توصیفِ اجراییه‌های ‏‎IO‎‏ اونها رو اجرا نمی‌کنه، درست همونطور که دستورِپختِ کیک به‌خودیِ‌خود کیک نمیده.*

در این محیط که مقدار ندارین، و در واقع فقط راهِ گرفتن یه مقدار رو دارین، مفهومی نداره بگیم میشه اون مقدار رو به اشتراک گذاشت.

وقتشه

پس یکی از مشخصاتِ کلیدیِ ‏‎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‎‏‌ها آماده بودین، برین سراغ کتابِ سایمون مارلو.