۲۹ - ۳این ماشین برنامه‌ها رو می‌کُشه

کدِ خالص هم ممکنه استثنا بده:

Prelude> 2 `div` 0
*** Exception: divide by zero

اما اجرای کد، یه اجراییه‌ ِ I/O ِه (و GHCi بصورتِ ضمنی ‏‎IO‎‏ رو فراخوان می‌کنه)، پس اکثرِ مواقعی که باید نگرانِ استثنا باشین، در ‏‎IO‎‏ هستین. حتی اگه در کدِ خالص رُخ بِدَن، فقط در ‏‎IO‎‏ میشه استثناها رو گرفت یا مدیریت کرد.

‏‎IO‎‏ شاملِ یه جور قراردادِ ضمنی ِه: نمی‌تونین انتظارِ موفقیتِ بی‌قیدوشرطِ این محاسبه رو داشته باشین. اما اینطور که معلومه، دنیای بیرون دنیای خیلی خَشِن ایه – هر جور اجراییه ِ ‏‎IO‎‏ ای ممکنه شکست بخوره، حتی ‏‎putStrLn‎‏.

اول نشون بدیم هر اجراییه ِ I/O ای ممکنه شکست بخوره. اینجا فرض بر اینه که در آدرسی که هستین، هیچ فایلی به اسمِ ‏‎aaa‎‏ وجود نداره. پس وقتی این کد رو اجرا می‌کنین، فایل رو می‌سازه، توش می‌نویسه، پیغام میده که این کار رو انجام داده (م. توی ترمینال چاپ می‌کنه "wrote to file")، و با موفقیت خاتمه پیدا می‌کنه:

-- writePls.hs
module Main where

main = do
  writeFile "aaa" "hi"
  putStrLn "wrote to file"

هم می‌تونین توی REPL بارگذاری‌ش کنین، هم می‌تونین اینطوری به یه فایلِ باینری کامپایل‌ِش کنین (البته اینها همه دوره‌اند، اگه بلدین خودتون انجام بدین):

$ stack ghc -- < filename > -o < output file name >
--             ^----------^    ^------------------^
 --               ‎‏اسم فایل‏‎         م. اسم فایل خروجی

و اینطوری اجراش کنین:

$ ./< output file name >

پس مثلاً اگه اسمِ فایلِ خروجی رو ‏‎wp‎‏ گذاشتین، جلسه‌ی ترمینال‌‌ِتون باید این شکلی بشه:

$ stack ghc -- writePls.hs -o wp
[برای کامپایل stack پیغام‌های]
$ ./wp
wrote to file
$ cat aaa
hi

خب، همه‌ش خوب کار کرد. یکی از دلایل‌ش این بود که ‏‎writeFile‎‏ یه فایل درست می‌کنه و بهش اجازه‌ی نوشتن میده (اگه قبلاً اون فایل وجود نداشته باشه). اما اگه سعی می‌کردین توی فایلی که از قبل وجود داره ولی اجازه‌ی نوشتن نداره بنویسین چطور؟

اول یه فایلِ فقط-خوندنی به نام ‏‎zzz‎‏ درست کنین تا آزمایش کنیم. در Linux یا OSX، با این دستورات میشه فایلی درست کرد که نمیشه توش نوشت:

$ touch zzz
$ chmod 400 zzz

فرض کنین این فایل در آدرسی قرار گرفته که می‌خوایم این کُد رو از توش اجرا کنیم:

-- writePls.hs
module Main where

main = do
  writeFile "zzz" "hi"
  putStrLn "wrote to file"

همون برنامه‌ایه که برای فایلِ ‏‎aaa‎‏ داشتیم، فقط اسم فایل عوض شده. باز هم می‌تونین مثل بالا، یا توی REPL بارگذاری‌ش کنین، یا باینری‌ش رو کامپایل کنین.

بعد اگه این برنامه رو اجرا کنین، نتیجه‌ی زیر رو می‌گیرین:

$ ./wp
wp: zzz: openFile:
  permission denied (Permission denied)
--                  ^-----------------^
--                   م. اجازه داده نشد

سطل‌مون یه سوراخ داره لایزا جان: یه استثنا.*

*

م. شوخی با شعرِ کودکانه‌ی There’s a Hole in my Bucket.

اگه می‌تونی منو بگیر

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

-- writePls.hs
module Main where

import Control.Exception
import Data.Typeable

handler :: SomeException -> IO ()
handler (SomeException e) = do
  print (typeOf e)
  putStrLn ("We errored! It was: " ++ show e)

main =
  writeFile "zzz" "hi"
    `catch` handler

به همون دلیلی که بالا هم نتونستیم توی فایل بنویسیم، این برنامه هم بدون نوشتن توی فایل خاتمه پیدا می‌کنه. برنامه اجرا و با موفقیت خاتمه پیدا می‌کنه، ولی خطا میده که با یه ‏‎IOException‎‏ شکست خورد. اینطوری اطلاعاتِ بیشتری از دلیلِ شکست ِ برنامه می‌گیریم، و اگه بخوایم می‌تونیم با استفاده از مدیریت استثنا مون (م. تابعِ ‏‎handler‎‏) اون اطلاعات رو یادداشت کنیم. گاهی اوقات این دقیقاً همون چیزیه که لازمه: اینکه اول برنامه استثنا رو گزارش بده و بعد بمیره. به زودی راه‌های دیگه‌ای از مدیریت ِ استثنا می‌بینیم که اجازه میدن برنامه با یه اجرا ِ جایگزین، کارِش رو ادامه بده.

فعلاً توجه‌مون رو بیاریم روی ‏‎catch‎‏:

catch :: Exception e
      => IO a
      -> (e -> IO a)
      -> IO a

اگه خاطرتون باشه، بالاتر به ‏‎catch‎‏ اشاره کردیم و گفتیم که ‏‎fromException‎‏ و ‏‎cast‎‏ رو برامون صدا میزنه. فقط در صورتی اجرا میشه که استثنا با تایپی که تعیین کردین جور باشه، و این فرصت رو میده که از خطا نجات پیدا کنه و نهایتاً تایپِ اصلی‌ای که اجراییه ِ ‏‎IO‎‏ قرار بود داشته باشه رو ارضا کنه. اگر هم هیچ استثنایی انداخته نشه، هیچ اتفاقی با اون ‏‎e‎‏ نمیوفته و ‏‎IO a‎‏ ِ اول با ‏‎IO a‎‏ ِ آخر یکی میشه.

حالا این مدیریتِ خطا ِ ساده رو ارتقا بدیم، تا بذاره برنامه بجای اینکه بمیره، یه مسیرِ جایگزین رو پیش بِره. این بار، باز هم اجراییه ِ ‏‎main‎‏‌ِمون می‌خواد توی اون فایلِ فقط-خوندنی بنویسه، اما این بار عهده‌گیرنده یه فایلِ جایگزین (که وجود نداره) بهش میده تا توی اون بنویسه (اگه فایلی به اسمِ ‏‎bbb‎‏ در آدرس ِ فعلی‌تون دارین، اسمی که به تابعِ ‏‎writeFile‎‏ دادیم رو تغییر بدین):

-- writePls.hs
module Main where

import Control.Exception
import Data.Typeable

handler :: SomeException -> IO ()
handler (SomeException e) = do
  print (typeOf e)
  putStrLn ("Running main caused an error!\
           \ It was: "
            ++ show e)
  writeFile "bbb" "hi"

main =
  writeFile "zzz" "hi"
    `catch` handler

وقتی نتونه توی ‏‎zzz‎‏ بنویسه، پیغامِ خطاش روی ترمینال چاپ میشه. آدرس‌تون هم که نگاه کنین، فایلِ جایگزین رو می‌بینین. اسم‌ش همون اسمی‌ه که توی ‏‎handler‎‏ تعیین کردین، و توش هم نوشته ‏‎hi‎‏.

حالا به یه کاربردی از ‏‎catch‎‏ نگاه کنیم که یه کم پیچیده‌تره. این کد رو از برنامه‌ای گرفتیم که (با استفاده از کتابخونه ِ ‏‎twitter-conduit‎‏) چیزهایی رو از اکانت ِ توییتر پاک می‌کنه. این بخشِ برنامه در صورتی شکست می‌خوره که برای صحبت با اکانت ِ توییتر ِ مورد نظر، به اطلاعاتِ هویتی‌ش دسترسی نداشته باشه. پس ما هم یه مدیریتِ استثنا درست می‌کنیم که بهش میگه در صورتِ چنین مشکلی چی کار کنه:

withCredentials action = do
  twinfo <-
    loadCredentials `catch` handleMissing
  case twinfo of
    Nothing     ->
      getTWInfo >>= saveCredentials
    Just twinfo -> action twinfo
  where handleMissing :: IOException
                      -> IO (Maybe TWInfo)
        handleMissing _ = return Nothing

یه ‏‎IOException‎‏ رو به ‏‎IO (Maybe a)‎‏ تبدیل می‌کنیم تا بتونیم با case کردن رویِ ‏‎Maybe‎‏، بگیم در حالتِ ‏‎Nothing‎‏ چه کاری انجام بده. توی این کد، اگه یه ‏‎IOException‎‏ بندازیم و مقدارِ ‏‎Nothing‎‏ برگردونیم، برنامه این رو اجرا می‌کنه:

getTWInfo >>= saveCredentials

با ذخیره‌ی اطلاعاتِ هویتی (کُدی که ذخیره‌سازی رو انجام میده اینجا نیاوردیم)، دفعه‌ی بعد که این کُد رو اجرا کنیم احتمالاً با این استثنا مواجه نمیشیم. در اون صورت، ‏‎action‎‏ ای که در خطِ ‏‎Just twinfo‎‏ نوشتیم رو اجرا می‌کنیم (اون اجراییه هم اینجا نیاوردیم، پوزش!).