۲۹ - ۲کلاسِ Exception و متودهاش
استثناها هم همون تایپها و مقادیری هستن که در طول کتاب دیدین. تایپهایی که استثناها رو کدبندی میکنن باید یه نمونه از تایپکلاسِ Exception
داشته باشن. سرآغازِ استثناهایی که امروز در هسکل هستن رو میشه در مقالهی سایمون مارلو در رابطه با سلسلهمراتب ِ قابلِ تعمیمی از استثناها که در زمان اجرا از هم متمایز میشن، مطالعه کرد. با استفاده از این سلسلهمراتب ِ قابلِ تعمیم، هم میشه استثناهایی که ممکنه تایپهای متفاوتی داشته باشن رو گرفت، و هم میشه در صورت نیاز، تایپهای استثنای جدیدی اضافه کرد.
تعریفِ تایپکلاسِ Exception
اینطوریه:
class (Typeable e, Show e) =>
Exception e where
toException :: e -> SomeException
fromException :: SomeException -> Maybe e
displayException :: e -> String
-- GHC.Exception تعریف شده در
محدودیت ِ Show
برای این وجود داره که بشه برای تایپی که e
بهش تعیین میشه، استثنا رو با فرمتی خوانا به صفحه چاپ کرد. Typeable
هم تایپکلاسیه که متودهایی برای شناساییِ تایپها در زمانِ اجرا تعریف میکنه. به زودی بیشتر راجع به این صحبت میکنیم و میگیم که چرا این محدودیتها برای تایپکلاسِ Exception
لازماند.
لیستِ تایپهایی که نمونه ِ Exception
دارن، خیلی بلنده:
-- بعضی نمونهها حذف شدن
instance Exception IOException
instance Exception Deadlock
instance Exception BlockedIndefinitelyOnSTM
instance Exception BlockedIndefinitelyOnMVar
instance Exception AsyncException
instance Exception AssertionFailed
instance Exception AllocationLimitExceeded
instance Exception SomeException
instance Exception ErrorCall
instance Exception ArithException
ما تکتکِ اینها رو توضیح نمیدیم. اما شاید خودتون بتونین کاربردِ بعضیهاشون رو حدس بزنین (مثلاً BlockedIndefinitelyOnMVar
). فقط میگیم که یه نوعداده با یک عضوه:
data BlockedIndefinitelyOnMVar =
BlockedIndefinitelyOnMVar
-- ^-----------------------^
-- MVar م. بلوکهی ابدی روی
-- GHC.IO.Exception تعریف شده در
اگه نگاهی به ArithException
بندازیم، میبینیم یه تایپِ جمع با چندین مقداره:
data ArithException
= Overflow
| Underflow
| LossOfPrecision
| DividedByZero
| Denormal
| RatioZeroDenominator
instance Exception ArithException
اگه ماژول ِ Control.Exception
رو وارد کنین و یه کم با دادهسازهای ArithException
بازی کنین، میبینین که مقادیرِ معمولیاند، و اصلاً غیرعادی نیستن.
اما انگار اینجا اتفاقاتِ دیگهای میوفته
همهی اینها رو باز میکنیم تا ببینیم بخشهای مختلف چطور با هم کار میکنن. اول نگاهی به متودهای تایپکلاسِ Exception
بندازیم:
toException :: e -> SomeException
fromException :: SomeException -> Maybe e
زیاد پیش نمیاد که از خودِ تابعهای toException
و fromException
استفاده کنیم. در عوض از تابعهای دیگهای که اونها رو صدا میزنن استفاده میکنیم. متود ِ toException
خیلی شبیهِ دادهساز ِ SomeException
ِه. شاید متوجه شدین که SomeException
هم توی اون لیستِ تایپهایی که نمونه ِ Exception
داشتن بود، حالا هم اینجا توی متودهای Exception
ِه. یه کم به نظر حلقهای میاد، اما نهایتاً SomeException
کلیدِ مدیریت ِ استثناهاست.
یه معرفی مختصر از سورِ وجودی
SomeException
یه جورایی نقشِ تایپِ والد رو برای بقیهی تایپهای استثنا بازی میکنه. به همین خاطر میشه در آنِ واحد تایپهای استثنای زیادی رو مدیریت کرد؛ بدون اینکه لازم باشه روی تکتکِشون تطبیق بدیم. ببینیم چطوری:
data SomeException where
SomeException
:: Exception e => e -> SomeException
شاید در نگاه اول به نظر عجیب نیاد. دلیلش اینه که بخشِ عجیبغریبش توی یه ساختاری به اسمِ GADT (مخففِ نوعدادهی جبریِ تعمیمیافته) مخفی شده. بطور کلی، GADTها در حوصلهی این کتاب نیستن. برای سرگرمی و یاد گرفتنِ هسکل در سطح متوسط خوباند، ولی برای برنامهنویسی با هسکل واجب نیستن. اون چیزی که گرامر ِ GADT مخفی کرده، چیزی ِه به اسم سورِ وجودی.
میشد تایپِ SomeException
رو بدون اینکه مفهومش تغییری کنه اینطوری بنویسیم:
data SomeException =
forall e . Exception e => SomeException e
به صورتِ عادی، forall
متغیرها رو بصورتِ عمومی سور میکنه (شاید از لغتِ all
حدس زده بودین). اما نوعساز ِ SomeException
آرگومان نمیگیره؛ اون متغیرِ تایپیِ e
یه پارامتر برای دادهساز ِه. انتقالِ سوردهنده به دادهساز، گسترهی کاربردش رو محدود میکنه، و معنای for all e رو به there exists some e تغییر میده (م. از به ازای هر e به وجود دارد e ای). این سورِ وجودی ِه. معنیش اینه که هر تایپی که دارای تایپکلاسِ Exception
باشه، میتونه اون e
باشه و زیرِ تایپِ SomeException
مشمول بشه.
ما اینجا سور وجودی رو عمیق بررسی نمیکنیم؛ فقط داریم مزهمزه میکنیم. معمولاً وقتی نوعسازها پارامتردار میشن، به صورت عمومی سور داده میشن. برای ارضای اونها، باید آرگومانهاشون تأمین بشن. تایپِ Maybe a
، همونطور که قبلاً هم اشاره کردیم، یه جور تابعیه که منتظرِ یه آرگومان برای تکاملِشه.
اما وقتی یه تایپ رو به صورتِ وجودی سور میکنیم (مثلِ SomeException
)، کارِ زیادی با اون متغیرِ تایپِ پلیمورفیک در دادهسازِش نمیشه انجام داد. نمیشه معینِش کرد. نمیتونیم چیزی ازش بدونیم، فقط میشه بهش محدودیت اضافه کنیم. باید پلیمورفیک بمونه. میشه هر مقداری از هر تایپی که اون محدودیتها رو رعایت میکنه بجاش بذاریم. مثل یه انگلِ پلیمورفیک میمونه که از تایپمون آویزونه.
پس اون e
میتونه هر تایپِ استثنایی باشه (هر تایپی که یه نمونه از تایپکلاسِ Exception
داشته باشه) و به عنوانِ یه SomeException
مدیریت بشه. همونطور که به زودی میبینیم، Typeable
و Show
رو برای تشخیصِ اینکه با چه تایپِ استثنایی کار میکنیم لازم داریم.
وایسا، چی شد؟
برای اینکه ببینیم سور وجودی اجازهی چه کارهایی رو میده، مثالی میزنیم که به جادویِ دَمودستگاهِ استثنای زمانِ اجرا وابسته نیست. اینجا خطاها رو در Either
ای از دوتایپِ کاملاً متفاوت برمیگردونیم، بدون اینکه مجبور باشیم هردوشون رو زیرِ یه تایپِ جمع متحد کنیم:
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE GADTs #-}
module WhySomeException where
import Control.Exception
( ArithException(..)
, AsyncException(..))
import Data.Typeable
data MyException =
forall e .
(Show e, Typeable e) => MyException e
instance Show MyException where
showsPrec p (MyException e) =
showsPrec p e
multiError :: Int
-> Either MyException Int
multiError n =
case n of
0 -> Left (MyException DividedByZero)
1 -> Left (MyException StackOverflow)
_ -> Right n
چیزی که در موردِ کُدِ بالا خاصه، اینه که در Either
حالتِ Left
شامل مقادیرِ خطا با دو تایپِ کاملاً متفاوته، بدون اینکه جداگانه در یه تایپِ جمع دستهبندیشون کنیم. به نظر نمیرسه تایپِ MyException
در نوعسازِش یه آرگومانِ پلیمورفیک داشته باشه، اما در دادهسازِش داره. به خاطرِ تایپِ e
که بصورتِ وجودی سور شده، میشه دادهساز ِ MyException
رو به مقادیرِ تایپهای مختلف اعمال کنیم.
data SomeError =
Arith ArithException
| Async AsyncException
| SomethingElse
deriving (Show)
discriminateError :: MyException
-> SomeError
discriminateError (MyException e) =
case cast e of
(Just arith) -> Arith arith
Nothing ->
case cast e of
(Just async) -> Async async
Nothing -> SomethingElse
runDisc n =
either discriminateError
(const SomethingElse) (multiError n)
بعد اگه امتحانش کنیم:
Prelude> runDisc 0
Arith divide by zero
Prelude> runDisc 1
Async stack overflow
Prelude> runDisc 2
SomethingElse
این چکیدهی دلیلیه که برای استثناها سورِ وجودی لازم داریم: تا بتونیم تایپهای استثنای مختلف رو بدون اینکه مجبور به تمرکز و اتحادِ اونها زیرِ یه تایپِ جمع باشیم، بندازیم. زیادی از این قابلیت استفاده نکنین.
قبل از این طراحی، چندتا راه برای مدیریتِ استثنا وجود داشت. راههای واضحش استفاده از یه تایپِ جمع ِ گنده، یا نوشته بود. مشکل اینجاست که هیچ کدوم از اونها رو نمیشه به خوبی به نوعدادههای درستحسابی و ساختاردار گسترش داد. ما در واقع یه جور سلسلهمراتب از مقادیر میخوایم، طوری که گرفتنِ والد معادلِ گرفتنِ هرکدوم از بچهها ِ ممکن هم باشه. با ترکیبِ SomeException
با تایپکلاسِ Typeable
میشه استثناهای مختلف از تایپهای مختلف رو انداخت، و بعد بدون پوشوندنِ همهشون توی یه تایپِ جمع، بعضیها یا همهشون رو در یک بهعهدهگیرنده گرفت.
Typeable
تایپکلاسِ Typeable
در ماژول ِ Data.Typeable
تعریف شده. Typeable
به تایپها اجازه میده تا در زمان اجرا شناخته بشن، یعنی یه جور تایپچِکینگِ دینامیک. علاوه بر اینکه اجازه میده در زمان اجرا تایپِ یک مقدار رو بشناسین، میذاره تایپِ دو مقدار رو با هم مقایسه کنین تا ببینین یکی هستن یا نه.
بطور معمول چنین کاری خوب نیست، اما وقتی صحبت از استثناها باشه، کارِ معقولیه. برای مدیریتِ استثنا، باید بتونیم چک کنیم آیا مقادیرِ با تایپهای (به احتمال) متنوع با اون تایپِ Exception
ای که میخوایم مدیریت کنیم جور هست یا نه؛ و باید بتونیم این کار رو در زمان اجرا انجام بدیم، یعنی زمانی که استثنا رخ میده. پس این شهودِ زمانِ اجرا برای تایپهای استثنا لازمه.
یه نگاه به نسخهی ساده شدهی متود ِ cast
از تعریفش در base
بندازیم:
cast :: (Typeable a, Typeable b)
=> a -> Maybe b
معمولاً این تابع رو بطور مستقیم صدا نمیزنیم، معمولاً تابعِ fromException
صداش میزنه، تابعِ fromException
هم تابعِ catch
صدا میزنه.
در زمان اجرا، وقتی استثنایی انداخته میشه، توی stack (م. یا دستهی تابعها) قِل میخوره عقب، و دنبالِ یه catch
میگرده. وقتی یه catch
پیدا میکنه، چک میکنه ببینه این catch
چه تایپِ استثنایی گیر میاره. توابع fromException
و cast
رو صدا میزنه تا ببینه تایپِ استثنایی که انداخته شده بود با یکی از تایپهای استثنایی که با catch
مدیریت میکنیم جور هست یا نه. تابعِ catch
ای که یه SomeException
رو به عهده میگیره، به خاطر انعطافپذیریِ اون تایپ، هر تایپِ استثنا ِ دیگهای رو هم به عهده میگیره.
اگه با هم جور نشن، یه مقدارِ Nothing
میگیریم؛ استثنا باز توی stack قل میخوره و دنبالِ یه catch
ای میگرده که بتونه استثنایی که انداخته شده بود رو مدیریت کنه. اگر هم نتونه پیدا کنه، برنامهتون بطور ناخوشایندی میمیره.
اگه با هم جور بشن، Just a
اجازه میده استثنا رو بگیریم.