۲۹ - ۶چرا throwIO به‌درد می‌خوره؟

شاید وجودِ چیزی مثلِ ‏‎thowIO‎‏ به نظرتون عجیب اومده باشه (شاید هم نه!). اصلاً چرا بخوایم یه برنامه رو عمداً با یه استثنا متوقف کنیم؟ در دنیای واقعی زیاد لازم میشه که بخوایم در صورتِ محیا شدنِ شرایطی، برنامه متوقف بشه؛ اما شاید دیدنِ چنین چیزی از مثال‌هایی که تا اینجا زدیم آسون نباشه.

تابعی به اسمِ ‏‎throw‎‏ وجود داره که اون هم استثناهایی مثلِ ‏‎ArithException‎‏ میندازه، اما به ندرت استفاده میشه. چیزی‌ه که به تابعِ ‏‎div‎‏ امکانِ انداختنِ یه ‏‎DivideByZero‎‏ میده، اما خارج از این تابع‌های کتابخونه‌ای، لازم نمیشه.

میشه فرقِ بین ‏‎throw‎‏ و ‏‎throwIO‎‏ رو در تایپ دید:

throwIO :: Exception e => e -> IO a

میشه این "تبعیضی" که در فرمِ انداختنِ یه استثنا قائل میشیم رو یه اثر دونست. راه متعارف برای انداختنِ یه استثنا، استفاده از ‏‎throwIO‎‏ ِه، که در جواب‌ش ‏‎IO‎‏ داره. تنها تفاوت‌ش با ‏‎throw‎‏ همینه که استثنا رو در ‏‎IO‎‏ می‌پوشونه. استثناها همیشه در ‏‎IO‎‏ مدیریت میشن.* مدیریتِ استثناها باید داخلِ ‏‎IO‎‏ انجام بشه، حتی اگه بدون یه تایپِ ‏‎IO‎‏ انداخته شده باشن. تقریباً هیچ وقت ‏‎throw‎‏ لازم نمیشه، چون بدونِ هیچ هشداری در تایپ (حتی ‏‎IO‎‏استثنا رو میندازه.

*

چرا؟ چون گرفتن و مدیریتِ استثناها معادلِ اینه که از یک ورودی، میشه جواب‌های مختلفی تولید کرد. یعنی شفافیتِ مرجع رو نقض می‌کنه.

یه مثال میزنیم که توی ‏‎IO‎‏ همینطوری یه استثنا انداخته میشه تا تأثیرش روی جریانِ برنامه رو ببینین:

import Control.Exception

main :: IO ()
main = do
  throwIO DivideByZero
  putStrLn "lol"

Prelude> main
*** Exception: divide by zero

‏‎throwIO‎‏ هم مثلِ ‏‎throw‎‏ معمولاً در پشتِ صحنه توسط تابع‌های کتابخونه‌ای صدا زده میشه. در تعامل با دنیای واقعی، اکثراً لازم میشه در شرایطِ بخصوصی، برنامه وایسه یا یه پیغامِ خطا بده و بگه که یه چیزهایی درست پیش نرفتن. به دوتا مثال از کدهای واقعی نگاه می‌کنیم (از کتابخونه ِ ‏‎http-client‎‏ توسطِ مایکل اسنوی‌من) که وقتی چیزهای ‏‎http‎‏ همونطوری که می‌خواستیم پیش نرفتن، با استفاده از ‏‎throwIO‎‏ استثنا میندازه:

connectionReadLine :: Connection
                   -> IO ByteString
connectionReadLine conn = do
  bs <- connectionRead conn
  when (S.null bs) $
    throwIO IncompleteHeaders
  connectionReadLineWith conn bs

در کُدِ بالا، وقتی ‏‎ByteString‎‏ خالی باشه، ‏‎throwIO‎‏ یه استثنا ِ ‏‎IncompleteHeaders‎‏ میندازه. در مثالِ بعدی، وقتی پاسخ خیلی طول بکشه (یا time out بشه)، یه استثنا ِ ‏‎ResponseTimeout‎‏ میندازه:

parseStatusHeaders :: Connection
                   -> Maybe Int
                   -> Maybe (IO ())
                   -> IO StatusHeaders
parseStatusHeaders conn timeout' cont
    | Just k <- cont =
        getStatusExcpectContinue k
    | otherwise =
        getStatus
  where
    withTimeout = case timeout' of
      Nothing -> id
      Just  t ->
        timeout t >=>
        maybe
          (throwIO ResponseTimeout)
          return
    -- ... بقیه‌ی کد رو حذف کردیم ...

بدونِ دونستنِ نحوه‌ی پیاده‌سازیِ استثناها هم میشه از ‏‎http-client‎‏ استفاده کرد. ولی گاهی اوقات هم چنین چیزی لازم میشه، پس در بخشِ بعد به ساختنِ تایپ‌های استثنای خودمون نگاه کنیم. فقط دقت کنین که از لحظه‌ی نوشتن این کتاب، ‏‎http-client‎‏ نحوه‌ی تعریف و انداختنِ استثناها رو تغییر داده، ولی با این حال مثال‌ها باید مفید باشن.