۲۹ - ۳این ماشین برنامهها رو میکُشه
کدِ خالص هم ممکنه استثنا بده:
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
نوشتیم رو اجرا میکنیم (اون اجراییه هم اینجا نیاوردیم، پوزش!).