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