۲۹ - ۲کلاسِ 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 اجازه میده استثنا رو بگیریم.