۶ - ۶تایپکلاس Num

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

class Num a where
  (+) :: a -> a -> a
  (*) :: a -> a -> a
  (-) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a

و لیستِ نمونه‌هاش (کامل نیست):

instance Num Integer
instance Num Int
instance Num Float
instance Num Double

بیشترِ این اطلاعات رو قبلاً دیدیم: توابعِ حساب رو با تایپ سیگنچر ِشون در بالا نوشتیم (تابعِ ‏‎fromInteger‎‏ مثل ‏‎fromIntegral‎‏ می‌مونه با این تفاوت که بجای همه‌ی ‏‎Integral‎‏ ها فقط به ‏‎Integer‎‏ محدود شده)، پایین‌تر هم یه لیست از تایپ‌هایی که این تایپکلاس رو دارن – تایپ‌های عددی‌ای که قبلاً دیدیم. چیزی جدید نیست.

تایپکلاسِ ‏‎Integral‎‏

این تایپکلاس تعریفِ زیر رو داره:

class (Real a, Enum a) => Integral a where
  quot :: a -> a -> a
  rem :: a -> a -> a
  div :: a -> a -> a
  mod :: a -> a
  quotRem :: a -> a -> (a, a)
  divMod :: a -> a -> (a, a)
  toInteger :: a -> Integer

محدودیت تایپکلاسی ِ ‏‎(Real a, Enum a) =>‎‏ میگه که هر تایپی که تایپکلاسِ ‏‎Integral‎‏ داره، باید هر دو تایپکلاس‌های ‏‎Real‎‏ و ‏‎Enum‎‏ رو هم داشته باشه. اون گرامر ِ توپل، دقیقاً عطفِ منطقی ِ محدودیت‌های تایپکلاسی رو روی متغیرهای تایپ اعمال می‌کنه. یه تایپِ ‏‎Integral‎‏، هم باید یه عددِ حقیقی باشه و هم باید قابل شمارش باشه، بنابراین می‌تونه از متودهای هر دوی اون تایپکلاس‌ها بهره ببره. تایپکلاسِ ‏‎Real‎‏ هم تایپکلاسِ ‏‎Num‎‏ رو لازم داره. پس تایپکلاس ‏‎Integral‎‏ متودهای ‏‎Real‎‏ و ‏‎Num‎‏ رو در اختیار داره (به اضافه‌ی متودهای ‏‎Enum‎‏). از اونجا که ‏‎Real‎‏ نمی‌تونه متودهای ‏‎Num‎‏ رو پایمال کنه، این وراثت ِ تایپکلاس‌ها فقط افزایشی ِه و در نتیجه ابهاماتِ ناشی از چند وراثتی (معروف به "الماس کشنده‌ی مرگ") که در بعضی زبان‌های دیگه وجود داره از بین میره.

تمرین‌ها: آزمایش توپل

تایپِ توابعِ ‏‎quotRem‎‏ و ‏‎divMod‎‏ رو نگاه کنین. فکر می‌کنین چه کاری انجام میدن؟ تو REPL باهاشون بازی کنین و ببینین نظریه‌تون درسته یا نه. یه مثال برای شروع کار نوشتیم:

Prelude> let ones x = snd (divMod x 10)

تایپکلاسِ ‏‎Fractional‎‏

‏‎Num‎‏ اَبرکلاس ِ ‏‎Fractional‎‏ ِه. تایپکلاسِ ‏‎Fractional‎‏ اینطوری تعریف میشه:

class (Num a) => Fractional a where
  (/)          :: a -> a -> a
  recip        :: a -> a
  fromRational :: Rational -> a

این تعریفِ تایپکلاس، یه کلاس به اسم ‏‎Fractional‎‏ می‌سازه که برای ساختنِ نمونه‌ش، آرگومانِ تایپی‌ش (‏‎a‎‏) باید یه نمونه از ‏‎Num‎‏ داشته باشه. این یه مثالِ دیگه از وراثت ِ تایپکلاس‌هاست. ‏‎Fractional‎‏ نسبت به ‏‎Num‎‏، به اعدادِ کمتری اعمال میشه و نمونه‌های ‏‎Fractional‎‏ می‌تونن از توابعِ تعریف شده در ‏‎Num‎‏ استفاده کنن، ولی از اونجا که هیچ چیزی در تعریفِ ‏‎Num‎‏ ملزوم به داشتنِ ‏‎Fractional‎‏ نیست، همه‌ی ‏‎Num‎‏ ها به توابعِ ‏‎Fractional‎‏ دسترسی ندارن. انتهای فصل یه نمودار هست که به درک بصری این روابط کمک می‌کنه.

با توابع عادی هم میشه دید:

اول این تابع رو در نظر بگیریم (عمداً تایپ‌ش رو ننوشتیم):

divideThenAdd x y = (x / y) + 1

این تابع رو با تایپی که فقط ‏‎Num‎‏ داشتن رو الزام می‌کنه بارگذاری می‌کنیم:

divideThenAdd :: Num a => a -> a -> a
divideThenAdd x y = (x / y) + 1

و خطای تایپ می‌گیریم:

Could not deduce (Fractional a)
  arising from a use of ‘/’
from the context (Num a)
  bound by the type signature for
    divideThenAdd :: Num a => a -> a -> a

حالا اگه فقط محدودیت ِ ‏‎Num‎‏ برامون مهم بود، میشد تابعِ ‏‎(/)‎‏ رو که ‏‎Fractional‎‏ لازم داره از تابع‌مون حذف کنیم:

-- این کار می‌کنه
-- هم (+) و هم (-) رو تأمین می‌کنه Num

subtractThenAdd :: Num a => a -> a -> a
subtractThenAdd x y = (x - y) + 1

یا می‌تونیم بجای خودِ تابع، فقط تایپ‌ش رو عوض کنیم:

-- این هم کار می‌کنه 

divideThenAdd :: Fractional a => a -> a -> a
divideThenAdd x y = (x / y) + 1

کلاه تفکرتون رو بپوشین

چرا لازم نبود هر دو تایپکلاس رو برای تابع‌مون اجبار کنیم؟ چرا اینطوری ننوشتیم:

f :: (Num a, Fractional a) => a -> a -> a

به مفهومِ اینکه یه چیزی زیرمجموعه‌ی یه مجموعه‌ی بزرگتری از اشیا باشه فکر کنین.