۴ - ۴تایپ‌های عددی

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

اعدادِ صحیح

اعدادِ تام یا کامل‌اند، چه مثبت و چه منفی.

۱.

تایپِ ‏‎Int‎‏: این تایپ، عددِ صحیح با دقتِ ثابت ِه. منظور از دقتِ ثابت اینه که محدوده دارن، با یه مقدارِ حداکثر و یه مقدارِ حداقل؛ و نمی‌تونن هر چقدر که می‌خوایم بزرگ یا کوچک باشن – کمی جلوتر بیشتر توضیح میدیم.

۲.

تایپِ ‏‎Integer‎‏: این تایپ هم برای اعدادِ صحیح ِه، ولی اعدادش محدوده‌ای ندارن (هر چقدر که بخوایم بزرگ یا کوچک میشن).

اعدادِ کسری

این اعداد صحیح نیستن. مقادیرِ ‏‎Fractional‎‏ شامل این چهار تایپ‌اند:

۱.

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

۲.

تایپِ ‏‎Double‎‏: تایپی برای اعداد با ممیز شناور ِ دو-دقتی. مشابهِ تایپِ ‏‎Float‎‏، با تفاوت اینکه دو برابر بیت برای توصیفِ اعداد دارن.

۳.

تایپِ ‏‎Rational‎‏: نوعی عددِ کسری که نسبت ِ دو عددِ صحیح رو نشون میده. مقدارِ ‏‎5 / 7 :: Rational‎‏ حامِل دو مقدارِ ‏‎Integer‎‏ ِه: یکی صورتِ ۵، و اون یکی مخرجِ ۷. ‏‎Rational‎‏ دقتِ نامحدود داره، ولی به اندازه‌ی ‏‎Scientific‎‏ مقرون به صرفه نیست.

۴.

تایپِ ‏‎Scientific‎‏: نوعی از اعداد که فضای مناسبی رو اشغال می‌کنن، و دقت تقریباً نامحدودی دارند. اعدادِ ‏‎Scientific‎‏ با نماد علمی نشون داده میشن، و متشکل از یک ضریب از تایپِ ‏‎Integer‎‏، و یک توان از تایپِ ‏‎Int‎‏ هستن. به خاطرِ محدودیتِ ‏‎Int‎‏، در حقیقت اعدادِ ‏‎Scientific‎‏ هم محدود اند، ولی خیلی کم پیش میاد به اون حد برسیم. تایپِ ‏‎Scientific‎‏ در یه کتابخونه تعریف شده، و میشه با دستورِ ‏‎cabal install‎‏ یا ‏‎stack install‎‏ نصب‌ش کرد.

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

با تایپکلاس‌ها میشه قابلیت‌هایی رو به تایپ‌ها اضافه کرد که برای همه‌ی تایپ‌های دارای یه نمونه از اون تایپکلاس، قابلِ استفاده باشن. ‏‎Num‎‏ تایپکلاسی‌ه که اکثر تایپ‌های عددی یک نمونه ازَش دارند، دلیل‌ش وجودِ توابع استانداردی‌ه که برای همه‌ی تایپ‌های عددی قابلِ استفاده‌اند. تایپکلاسِ ‏‎Num‎‏ چیزیه که عملگرهای استاندارد مثل ‏‎(+)‎‏، ‏‎(-)‎‏، و ‏‎(*)‎‏ و چندتای دیگه رو در اختیارِ اعداد قرار میده. میشه این توابع رو به هر تایپی که یه نمونه از تایپکلاس ‏‎Num‎‏ داره اعمال کنیم. یک نمونه، چگونگیِ عملکردِ توابع برای یک تایپِ بخصوص رو تعریف می‌کنه. به زودی تایپکلاس‌ها رو با جزئیاتِ خیلی بیشتر توضیح میدیم.

اعداد صحیح

همونطور که بالاتر گفتیم، اعدادِ صحیح، دو تایپِ اصلی دارن: ‏‎Int‎‏ و ‏‎Integer‎‏.

اعداد صحیح، اعدادِ کامل و بدونِ اعشارند. مثل اینها:

1 2 199 32442353464675685678

اما اینها integral نیستند:

1.3 1/2

‏‎Integer‎‏

اینها همون اعدادِ کاملی‌اند که بهشون عادت داریم. می‌تونن مثبت یا منفی باشن، و هر چقدر هم لازم باشه بزرگ یا کوچک میشن.

تایپِ ‏‎Bool‎‏ فقط دو مقدار داره که میشه صراحتاً به عنوانِ داده‌ساز بنویسیمِ‌شون. در موردِ ‏‎Integer‎‏، و بیشترِ نوع‌دادههای عددی که بینهایت مقدار دارن، داده‌سازهاشون نوشته نمیشن. از نظر تئوری، یک ‏‎Integer‎‏ رو میشه با جمع ِ سه حالتِ: صفر، سازنده‌های بازگشتی به سمتِ منفیِ بینهایت، و سازنده‌های بازگشتی به سمتِ مثبتِ بینهایت تعریف کرد. ولی چنین رَوِشی اصلاً مقرون به صرفه نیست، به همین خاطر، در این مورد GHC یه کم جادو می‌کنه.

چرا ‏‎Int‎‏ داریم؟

تایپِ عددیِ ‏‎Int‎‏ در واقع بازمونده‌ای از امکاناتِ اولیه‌ی کامپیوترهاست. اکثر برنامه‌ها باید با ‏‎Integer‎‏ کار کنن، نه ‏‎Int‎‏؛ مگر اینکه برنامه‌نویس احاطه‌ی کامل به محدودیتهای ‏‎Int‎‏ داشته باشه، و اینکه عملکرد ِ اضافه‌ی حاصل از اونها واقعاً مطلوب باشه.

خطرِ استفاده از ‏‎Int‎‏ (و تایپ‌های مشابه مثلِ ‏‎Int8‎‏، ‏‎Int16‎‏ و غیره) اینه که نمی‌تونن اطلاعاتِ خیلی بزرگ رو بَیان کنن. پس از اونجا که اینها اعدادِ صحیح هستن، نمی‌تونن در جهت مثبت یا منفی اندازه‌ی دلخواه داشته باشن.

تو این مثال می‌بینید که وقتی از عددی بزرگتر از حدّ ‏‎Int8‎‏ استفاده می‌کنیم، چه اتفاقی میوفته:

Prelude> import GHC.Int
Prelude> 127 :: Int8
127
Prelude> 128 :: Int8

<interactive>:11:1: Warning:
    Literal 128 is out of the
      Int8 range -128..127
    If you are trying to write a large
      negative literal,
    use NegativeLiterals
-128
Prelude> (127 + 1) :: Int8
-128

گرامری که اینجا می‌بینید، ‏‎:: Int8‎‏، وظیفه‌ی تخصیصِ تایپِ ‏‎Int8‎‏ به این اعداد رو داره. همونطور که در فصلِ بعد می‌بینیم، اعداد در باطن پلی‌مورفیک اند، و کامپایلر تا زمانی که مجبور نباشه، هیچ تایپِ معیّنی بهشون اختصاص نمیده. یه کم عجیب و غیرمنتظره میشد اگه پیش‌فرض ِ کامپایلر تخصیص تایپِ ‏‎Int8‎‏ به همه‌ی اعداد بود. ما هم به همین خاطر با اون گرامر، تایپِ معیّن ِ ‏‎Int8‎‏ رو به اعدادمون اختصاص دادیم.

همونطور که می‌بینید، ۱۲۷ در بازه‌ی ‏‎Int8‎‏ هست و موردی نداره. ۱۲۸ یه هشدار از سرریز شدن داد، ۱ + ۱۲۷ هم سرریز شد و برگشت به کوچکترین مقدارِ عددی. به خاطرِ محدود بودنِ حافظه‌ای که مقادیرِ ‏‎Int8‎‏ می‌تونن اشغال کنن، توانایی تطابق با عددی مثل ۱۲۸ رو هم ندارن (برعکسِ ‏‎Integer‎‏). عددِ ۸ نشون دهنده‌ی تعداد بیت‌هایی‌ه که تایپ برای ارائه‌ی اعدادِ صحیح استفاده می‌کنه.* گاهی اوقات ممکنه اندازه‌ی ثابت ِ این تایپ‌ها مفید باشه، ولی عمدتاً ‏‎Integer‎‏ ارجح‌ه.

*

تایپ‌های ‏‎Int‎‏ که اندازه‌ی ثابت هستن، با رَوِشِ مکملِ دو اعدادشون رو ارائه میدن.

بیشترین و کمترین ِ مقادیرِ تایپ‌های عددی رو می‌تونین با استفاده از ‏‎minBound‎‏ و ‏‎maxBound‎‏ از تایپکلاسِ ‏‎Bounded‎‏ پیدا کنین. در زیر چندتا مثال زدیم:

Prelude> import GHC.Int
Prelude> :t minBound
minBound :: Bounded a => a
Prelude> :t maxBound
maxBound :: Bounded a => a

Prelude> minBound :: Int8
-128
Prelude> minBound :: Int16
-32768
Prelude> minBound :: Int32
-2147483648
Prelude> minBound :: Int64
-9223372036854775808

Prelude> maxBound :: Int8
127
Prelude> maxBound :: Int16
32767
Prelude> maxBound :: Int32
2147483647
Prelude> minBound :: Int64
9223372036854775807

اگه تایپی، این تایپکلاسِ بخصوص رو داشته باشه، می‌تونین محدوده‌ی مقادیرِ ممکن اون تایپ رو پیدا کنین. در مثال بالا دیدیم که بازه‌ی مقادیرِ قابل ارائه با ‏‎Int8‎‏ از ۱۲۸- تا ۱۲۷ بود.

با دستورِ ‏‎:info‎‏، می‌تونین همه‌ی تایپکلاس‌های یه تایپ رو تو GHCi ببینین. این دستور، تعریفِ نوع‌داده ای هم که استعلام کردین رو نشون میده. برای مثال، می‌خوایم بدونیم که آیا تایپِ ‏‎Int‎‏ نمونه‌ای از تایپکلاسِ ‏‎Bounded‎‏ داره یا نه:

Prelude> :i Int
data Int = GHC.Types.I# GHC.Prim.Int#
-- Defined in ‘GHC.Enum’
instance Bounded Int

البته ‏‎Int‎‏ نمونه‌های تایپکلاسیِ خیلی بیشتری داره که اینجا ننوشتیم.

اعدادِ کسری

چهار تایپِ رایجِ اعدادِ ‏‎Fractional‎‏ در هسکل ‏‎Float‎‏، ‏‎Double‎‏، ‏‎Rational‎‏، و ‏‎Scientific‎‏ اند. ‏‎Float‎‏، ‏‎Double‎‏، و ‏‎Rational‎‏ از ابتدا توی GHC هستن. اما ‏‎Scientific‎‏، همونطور که گفتیم از یه کتابخونه میاد. ‏‎Rational‎‏ و ‏‎Scientific‎‏ هر دو دقتِ نامحدود دارن، ولی ‏‎Scientific‎‏ بهینه‌تَر ِه. منظور از این نامحدود بودن اینه که از این دو تایپ، بر خلافِ ‏‎Float‎‏ و ‏‎Double‎‏ که به یه دقتِ مشخص محدوداند، میشه برای محاسباتی که نیاز به دقتهای خیلی بالا دارن استفاده کرد. تقریباً هیچ وقت ‏‎Float‎‏ لازم نمیشه، مگر برای کارهایی مثل برنامه‌نویسیِ گرافیکی با OpenGL.

بعضی محاسبه‌های عددی با اعدادِ کسری کار می‌کنن. یه مثالِ خوب، تابع تقسیم ‏‎(/)‎‏ ِه، که تایپِ‌ش اینه:

Prelude> :t (/)
(/) :: Fractional a => a -> a -> a

نوشته‌ی ‏‎Fractional a =>‎‏ یه محدودیتِ تایپکلاسی ِه. یعنی متغیرِ ‏‎a‎‏ مقیّد به داشتنِ تایپکلاسِ ‏‎Fractional‎‏ ِه. مهم نیست ‏‎a‎‏ چه تایپ عددی باشه، فقط باید یک نمونه از تایپکلاسِ ‏‎Fractional‎‏ داشته باشه؛ به عبارت دیگه، باید یک تعریف وجود داشته باشه که عملیات‌های اون تایپکلاس رو برای اون تایپ توصیف کنه. تابعِ ‏‎/‎‏ یک عددی که تایپکلاس ‏‎Fractional‎‏ داره رو می‌گیره، به یه عددِ دیگه از همون تایپ تقسیم می‌کنه، و یه مقدار از همون تایپ رو به عنوان جواب برمی‌گردونه.

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

این نتیجه‌ی استفاده از ‏‎(/)‎‏ در REPL ِه:

Prelude> 1 / 2
0.5
Prelude> 4 / 2
2.0

دقت کنید با اینکه جوابمون یه عدد کامل بود، باز هم کسری شد. دلیل ِش اینه که مقادیرِ ‏‎Fractional a => a‎‏ به صورتِ پیش‌فرض به تایپِ ‏‎Double‎‏ تبدیل میشن. اکثر مواقع، بهتره صراحتاً از ‏‎Double‎‏ استفاده نکنین، ‏‎Scientific‎‏ با دقت نامحدودی که داره (میشه گفت داداشِ ‏‎Integer‎‏ ِه!)، گزینه‌ی بهتریه. اکثر مردم با محاسباتِ ممیز شناور، به خاطرِ رفتارهای نامتعارف‌ش، راحت کنار نمیان (اون رفتارها به عمد طراحی شدن، ولی اون یه مبحثِ دیگه‌ست)؛ شما هم برای آسودگیِ خاطر، از تایپ‌های با دقتِ نامحدود استفاده کنین.