۲۹ - ۴آیدِر میخوای؟ 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 جونِ سالم به در بردیم!