۲۶ - ۸ثانک یا thunk

از ثانک برای اشاره به محاسباتِ معلقی که ممکنه جلوتر در برنامه محاسبه یا اجرا بشن استفاده میشه. اگه بخواین می‌تونین این مبحث رو با جزئیاتِ خیلی بیشتری یاد بگیرین، اما بطور خلاصه، ثانک‌ها محاسباتی‌اند که هنوز تا حالت معمولی با سر ضعیف محاسبه نشدن. اگه مستنداتِ GHC رو بخونین ممکنه به لغتِ حالتِ معمولیِ سردار بر بخورین – فرقی با حالت معمولی با سر ضعیف نداره، همونه.

هر مقداری ثانک نمیشه

در این بخش از دستورِ ‏‎sprint‎‏ در GHCi برای نشون دادنِ اینکه کِی یه چیزی ثانک شده استفاده می‌کنیم. ممکنه از فصلِ لیست‌ها این دستور رو به خاطر داشته باشین، با این حال یه دوره می‌کنیم.

با دستورِ ‏‎sprint‎‏ توی REPL میشه دید چه چیزی در حال حاضر حساب شده. مقادیری که حساب نشدن با یه خط‌تیره نشون داده میشن. قبلاً گفتیم که این دستور خصلت‌های عجیبی داره؛ حالا توی این فصل بعضی از عواملِ این رفتارهای به ظاهر غیرقابل پیش‌بینی رو توضیح میدیم.

با یه مثال ساده شروع می‌کنیم:

Prelude> let myList = [1, 2] :: [Integer]
Prelude> :sprint myList
myList = [1,2]

وایسا ببینم – اون لیست که هیچ جا لازم نشده بود، چرا کامل حساب شده؟ این یه "اکیدی ِ فرصت‌طلبانه"‌ست. GHC داده‌ساز‌ها رو ثانک نمی‌کنه (در نتیجه اونها رو به تعویق نمیندازه). داده‌ساز‌ها ثابت اند، و همین توجیه می‌کنه که چنین بهینه‌سازی‌ای امنیت داره. داده‌ساز‌های اینجا یکی cons ِه ‏‎(:)‎‏، یکی ‏‎Integer‎‏ ِه، و یکی هم لیستِ خالیه – که همه‌شون ثابت اند.

مگه داده‌سازها تابع نیستن؟

داده‌ساز‌ها وقتی هنوز اعمال نشدن، شبیهِ توابع‌اند؛ ولی وقتی اعمال شدن، ثابت میشن. در مثال بالا، به خاطر اینکه همه‌ی داده‌ساز‌ها تماماً اعمال‌شده هستن، محاسبه به حالت معمولی با سر ضعیف معادلِ محاسبه‌ی همه چیزه، چون چیزی برای اعمال کردن نمونده.

برگردیم به ثانک.

گراف ِ مقادیرِ ‏‎myList‎‏ شبیهِ این میشه:

myList
  |
  :
 / \
1   :
   / \
  2   :
     / \
    3  []

اینجا هیچ ثانک ِ محاسبه‌نشده‌ای وجود نداره؛ اینها همه مقادیرِ نهایی هستن که به خاطر سپرده شدن. اما اگه پلی‌مورفیکتَرِش کنیم:

Prelude> let myList2 = [1, 2, 3]
Prelude> :t myList2
myList2 :: Num t => [t]
Prelude> :sprint myList2
myList2 = _

می‌بینیم که یه ثانک ِ حساب‌نشده توسط خط‌تیره در بالاترین سطحِ بیانیه ارائه میشه. به خاطر اینکه تایپ معین نیست، یه تابعِ ضمنی ِ ‏‎Num a -> a‎‏ در پشت‌پرده وجود داره که منتظر اعمال به یه چیزیه که تایپ‌ش رو معین کنه. اینجا چیزی که منجر به اون محاسبه بشه وجود نداره، پس کلِ لیست یه ثانک ِ حساب‌نشده باقی می‌مونه. به زودی میرسیم به جزئیاتِ نحوه‌ی محاسبه شدنِ محدودیت‌های تایپکلاسی.

یه کار دیگه که GHC فرصت‌طلبانه انجام میده، اینه که به محض رسیدن به یه محاسبه، ساده‌سازی رو متوقف می‌کنه:

Prelude> let xs = [1, 2, id 1] :: [Integer]
Prelude> :sprint xs
xs = [1,2,_]

با اینکه محاسبه‌ی خیلی پیش‌وپاافتاده‌ایه، GHC رَهاش می‌کنه. گراف ِ ثانک‌ِش اینطوری میشه:

 xs
  |
  :
 / \
1   :
   / \
  2   :
     / \
    _  []

حالا یه حالت دیگه رو ببینیم که ممکنه در نگاه اول گیج‌کننده باشه:

Prelude> let xs = [1, 2, id 1] :: [Integer]
Prelude> xs' = xs ++ undefined
Prelude> :sprint xs'
xs' = _

چه خبر شده؟؟ کلِ‌ش ثانک شده چون در حالت معمولی با سر ضعیف نیست. چرا در حالت معمولی با سر ضعیف نیست؟ چون بیرونی‌ترین عبارت یه داده‌ساز مثلِ ‏‎(:)‎‏ نیست. بیرونی‌ترین عبارت، تابعِ ‏‎(++)‎‏ ِه:

xs' = (++) _ _

برخلاف ظاهرش که میانوند ِه، تابع بیرونی‌ترین‌ه چون تابع اون لاندا ِه. آرگومان‌ها به بدنه ِ تابع پاس میشن تا محاسبه بشن.