۲۹ - ۷ساختِ استثناهای خودمون

اکثراً تایپ‌های استثنای خودمون رو لازم داریم، مثلِ ‏‎http-client‎‏. اینطوری اتفاقاتی که توی برنامه میوفتن رو دقیق‌تر مشخص می‌کنیم. برای دیدنِ طرزِ انجام این کار، با یه مثال کوچیک، یکی از دو خطا ِ ممکنِ یه تابعِ ساده رو ساتع کنیم:

module OurExceptions where

import Control.Exception

data NotDivThree =
  NotDivThree
  deriving (Eq, Show)
instance Exception NotDivThree

data NotEven =
  NotEven
  deriving (Eq, Show)
instance Exception NotEven

دقت کنین که نمونه‌های ‏‎Exception‎‏ قابلِ مشتق‌گیری اند – لازم نیست خودتون بنویسین. ادامه بدیم:

evenAndThreeDiv :: Int -> IO Int
evenAndThreeDiv i
  | rem i 3 /= 0 = throwIO NotDivThree
  | odd i = throwIO NotEven
  | otherwise = return i

بعد شرایط موفقیت و خطا رو میشه دید:

*OurExceptions> evenAndThreeDiv 0
0
*OurExceptions> evenAndThreeDiv 1
*** Exception: NotDivThree
*OurExceptions> evenAndThreeDiv 2
*** Exception: NotDivThree
*OurExceptions> evenAndThreeDiv 3
*** Exception: NotEven
*OurExceptions> evenAndThreeDiv 6
6
*OurExceptions> evenAndThreeDiv 9
*** Exception: NotEven
*OurExceptions> evenAndThreeDiv 12
12

با اینکه چنین کُدی رایجه، اما یه مشکلی داره. اگه بخوایم بدونیم چه ورودی یا ورودی‌هایی عاملِ خطا شدن چطور؟ باید محتوا اضافه کنیم!

اضافه کردنِ محتوا

بریم سراغ‌ش:

module OurExceptions where

import Control.Exception

data NotDivThree =
  NotDivThree Int
  deriving (Eq, Show)

instance Exception NotDivThree

data NotEven =
  NotEven Int
  deriving (Eq, Show)

instance Exception NotEven

evenAndThreeDiv :: Int -> IO Int
evenAndThreeDiv i
  | rem i 3 /= 0 = throwIO (NotDivThree i)
  | odd i = throwIO (NotEven i)
  | otherwise = return i

حالا وقتی خطا بگیریم، میدونیم چه ورودی‌ای باعث‌ش شده:

*OurExceptions> evenAndThreeDiv 12
12
*OurExceptions> evenAndThreeDiv 9
*** Exception: NotEven 9
*OurExceptions> evenAndThreeDiv 8
*** Exception: NotDivThree 8
*OurExceptions> evenAndThreeDiv 3
*** Exception: NotEven 3
*OurExceptions> evenAndThreeDiv 2
*** Exception: NotDivThree 2

یکی رو بگیر، همه رو بگیر

حالا احتمالاً می‌دونین چطوری این دوتا خطا رو بگیریم:

catchNotDivThree :: IO Int
                 -> (NotDivThree -> IO Int)
                 -> IO Int
catchNotDivThree = catch

catchNotEven :: IO Int
             -> (NotEven -> IO Int)
             -> IO Int
catchNotEven = catch

یا با ‏‎try‎‏:

Prelude> type EA e = IO (Either e Int)
Prelude> try (evenAndThreeDiv 2) :: EA NotEven
*** Exception: NotDivThree 2
Prelude> try (evenAndThreeDiv 2) :: EA NotDivThree
Left (NotDivThree 2)

اون تایپِ مستعار مفهومی نداره، فقط یه کم شلوغی‌ها رو کم می‌کنه. حالا هردو خطاها رو با تابعِ ‏‎catches‎‏ میشه مدیریت کرد:

catches :: IO a -> [Handler a] -> IO a

catchBoth :: IO Int -> IO Int
catchBoth ioInt =
  catches ioInt
  [ Handler
      (\(NotEven _) -> return maxBound)
  , Handler
      (\(NotDivThree _) -> return minBound)
  ]

استفاده از ‏‎maxBound‎‏ یا ‏‎minBound‎‏ در کُدِ واقعی خوب نیست. اتفاقاً، تایپِ ‏‎Handler‎‏ هم از همون کلکی که تایپِ ‏‎SomeException‎‏ برای مخفی کردن آرگومان‌های تایپی استفاده می‌کنه، برای پوشوندنِ مقادیر توی اون لیستِ هندلِرهای استثنا استفاده می‌کنه: سورِ وجودی.

data Handler a where
  Handler :: Exception e
          => (e -> IO a) -> Handler a
          -- Control.Exception تعریف شده در

به این خاطر می‌تونیم یه لیست از هندلرهایی که استثناهای متنوعی رو هندل می‌کنن درست کنیم چون تایپ‌های استثنا، تحتِ نوع‌داده ِ ‏‎Handler‎‏ سورِ وجودی دارن.

ولی اگه این به اندازه‌ی کافی راحت نباشه چطور؟ اگه یه خانواده از استثناهای مرتبط یا مشابه داشته باشیم که بخوایم گروهی بگیریم چطور؟ باید بریم سراغِ دوستِ قدیمی‌مون، تایپِ جمع!

module OurExceptions where

import Control.Exception

data EATD =
    NotEven Int
  | NotDivThree Int
  deriving (Eq, Show)
instance Exception EATD

evenAndThreeDiv :: Int -> IO Int
evenAndThreeDiv i
  | rem i 3 /= 0 = throwIO (NotDivThree i)
  | odd i = throwIO (NotEven i)
  | otherwise = return i

حالا برای گرفتنِ هر دو خطا فقط یه عهده‌گیرنده لازم داریم. بعدش هم می‌تونیم مثل نوع‌داده‌های معمولی، روی تایپ‌های استثنا تطبیق الگو کنیم:

Prelude> type EA e = IO (Either e Int)
Prelude> try (evenAndThreeDiv 0) :: EA EATD
Left (NotEven 0)
Prelude> try (evenAndThreeDiv 1) :: EA EATD
Left (NotDivThree 1)

به درد می‌خوره، نه؟ خلاصه اینکه در طراحیِ تایپ‌های خطا، از همون روش‌ها و قضاوت‌هایی که برای تایپ‌های معمولی استفاده می‌کنین، بهره ببرین. محتوا رو حفظ کنین تا هرکسی بتونه از روی تایپ‌ها بفهمه چه مشکلی رو می‌خواستین برطرف کنین.