۲۹ - ۴آیدِر میخوای؟ try کن!
بعضی اوقات مطلوبه که استثناها رو صراحتاً به داخلِ مقادیرِ Either
لیفت کنیم. کاملاً هم شدنیه، اما نمیشه مُنکرِ این شد که در طولِ فرایند، I/O اجرا کردیم. هیچ تضمینی هم نیست که همهی استثناها گرفته میشن. با این تابع میشه استثناهای ضمنی رو به یه Either
ِ صریح تبدیل کنیم:
-- Control.Exception
try :: Exception e
=> IO a
-> IO (Either e a)
بعد برای اینکه ازش استفاده کنیم، میتویم یه چیزی مثلِ کُدِ زیر بنویسیم (فقط دقت کنین، که این مثلِ مثالهای قبلی به یه باینری کامپایل نمیشه، چون یه اجراشدنی ِ Main
نیست؛ از GHCi استفاده کنین):
module TryExcept where
import Control.Exception
willIFail :: Integer
-> IO (Either ArithException ())
willIFail denom =
try $ print $ div 5 denom
ما اینجا نتیجه رو print
میکنیم چون استثناها رو فقط توی IO
میشه به عهده گرفت (از تایپهای try
و catch
هم مشخصه). اگه بهش ورودی بدین، چیزی مثلِ اینها میبینین:
Prelude> willIFail 1
5
Right ()
Prelude> willIFail 0
Left divide by zero
یه چیزی که باید به خاطر داشت اینه که استثناها در هسکل، مثلِ استثناها در بقیهی زبانها میمونن – یعنی دقیق نیستن. اگه بخشِ بخصوصی از کُد یه استثنا رو نگیره، همینطور قل میخوره تا اینکه یا گرفته بشه یا برنامه رو بکُشه.
اگه بخواین اون Right ()
چاپ نشه، یه راهِش اینه:
onlyReportError :: Show e
=> IO (Either e a)
-> IO ()
onlyReportError action = do
result <- action
case result of
Left e -> print e
Right _ -> return ()
willFail :: Integer -> IO ()
willFail denom =
onlyReportError $ willIFail denom
یا از catch
استفاده کنین:
willIFail' :: Integer -> IO ()
willIFail' denom =
print (div 5 denom) `catch` handler
where handler :: ArithException
-> IO ()
handler e = print e
بذارین بازِش کنیم. میخوایم از مثالهای بالا یه باینریِ قابلِ اجرا درست کنیم، ولی یه مشکلی هست: در یه اجراشدنی، main
نمیتونه آرگومان بگیره. پس برای اینکه بتونیم موقعِ صدا زدنِ main
بهش آرگومان بدیم، باید تغییراتی ایجاد کنیم. با تابعِ getArgs
از ماژول ِ System.Environment
میشه main
رو با آرگومان صدا زد:
module Main where
import Control.Exception
import System.Environment (getArgs)
willIFail :: Integer
-> IO (Either ArithException ())
willIFail denom =
try $ print $ div 5 denom
onlyReportError :: Show e
=> IO (Either e a)
-> IO ()
onlyReportError action = do
result <- action
case result of
Left e -> print e
Right _ -> return ()
testDiv :: String -> IO ()
testDiv d =
onlyReporterError $ willIFail (read d)
main :: IO ()
main = do
args <- getArgs
mapM_ testDiv args
شاید دلیلِ استفاده از mapM_
واضح نباشه، پس یه کم بازِش میکنیم. در واقع یه حالتِ خاصتر از تابعِ traverse
ِه که نتیجهی نهایی رو دور میندازه و فقط اثرات رو تولید میکنه. اینجا اون اثرات، نتیجهی نگاشتِ تابعِ testDiv
روی یه لیست از آرگومانها میشن – یا جوابِ یه تقسیم ِ موفق رو برمیگردونه، یا تایپِ یه استثنا رو.
مثل قبل، این رو هم به یه باینریِ قابل اجرا کامپایل میکنیم. اینطوری میشه بهش آرگومان داد:
$ stack ghc -- writePls.hs -o wp
[stack شلوغی]
$ ./wp 4 5 0 9
1
1
divide by zero
0
اگه بخواین همین کار رو از توی REPL انجام بدین، از دستورِ :main
استفاده کنین و همون آرگومانها رو بهش بدین:
Prelude> :main 4 5 0 9
1
1
divide by zero
0
دقت کنین حالا که استثنا مدیریت شده، دیگه جوابِ آخر رو هم میگیریم – از یه ArithException
جونِ سالم به در بردیم!