۶ - ۸تایپکلاس Ord

حالا به تایپکلاسِ ‏‎Ord‎‏ می‌پردازیم. قبلاً گفتیم که این تایپکلاس برای تایپِ چیزهایی‌ه که میشه با ترتیب قرار بگیرن. اگه از دستورِ ‏‎:info‎‏ برای ‏‎Ord‎‏ در REPL استفاده کنین، نمونه‌های خیلی زیادی براش می‌بینین. ما این لیست رو خلاصه می‌کنیم و فقط روی چندتا از اونها تمرکز می‌کنیم، ولی طبق معمول شما رو به گشت و گذارِ بیشتر تشویق می‌کنیم:

Prelude> :info Ord

class Eq a => Ord a where
  compare :: a -> a -> Ordering
  (<) :: a -> a -> Bool
  (>=) :: a -> a -> Bool 
  (>) :: a -> a -> Bool
  (<=) :: a -> a -> Bool
  max :: a -> a -> a
  min :: a -> a -> a

instance Ord a => Ord (Maybe a)
instance (Ord a, Ord b) => Ord (Either a b)
instance Ord Integer
instance Ord a => Ord [a]
instance Ord Ordering
instance Ord Int
instance Ord Float
instance Ord Double
instance Ord Char
instance Ord Bool 

می‌بینید که اون بالا یه محدودیتِ تایپکلاسی ِ دیگه داریم. ‏‎Ord‎‏ با ‏‎Eq‎‏ محدود شده چون اگه بخواین مثلاً المان‌های یه لیست رو مقایسه و مرتب کنین، یه راهی برای بررسی تساوی ِ اونها لازم دارین. پس ‏‎Ord‎‏ به ‏‎Eq‎‏ و متودهاش احتیاج داره. توابعِ این تایپکلاس با ترتیب‌بندی سروکار دارن. بعضی‌هاشون خروجیِ ‏‎Bool‎‏ میدن که تا حالا یه کم باهاشون بازی کردیم. ببینیم بعضی‌های دیگه چی کار می‌کنن:

Prelude> compare 7 8
LT
Prelude> compare 4 (-4)
GT
Prelude> compare 4 4
EQ
Prelude> compare "Julie" "Chris"
GT
Prelude> compare True False
GT
Prelude> compare True True
EQ

تابعِ ‏‎compare‎‏ برای همه‌ی تایپ‌هایی که تایپکلاس ‏‎Ord‎‏ دارن کار می‌کنه، مثل ‏‎Bool‎‏، اما بر خلاف توابعی مثل ‏‎<‎‏، ‏‎>‎‏، ‏‎>=‎‏، و ‏‎<=‎‏ بجای مقادیرِ ‏‎Bool‎‏، مقادیرِ ‏‎Ordering‎‏ برمی‌گردونه.*

*

م. مقادیرِ تایپِ ‏‎Ordering‎‏ از این قرارند: ‏‎LT‎‏، ‏‎EQ‎‏، یا ‏‎GT‎‏ که به ترتیب مخفف کوچکتر از، برابر، و بزرگتر از هستن.

شاید متوجه شده باشین که ‏‎True‎‏ از ‏‎False‎‏ بزرگتره. این به خاطر نحوه‌ی تعریفِ تایپ ‏‎Bool‎‏ ِه: ‏‎False | True‎‏. اگه بخواین از جنبه‌ی فلسفی بررسی کنین شاید دلایل عمیق‌تری هم پیدا کنین.

توابعِ ‏‎min‎‏ و ‏‎max‎‏ هم با همه‌ی تایپ‌هایی که تایپکلاسِ ‏‎Ord‎‏ دارن کار می‌کنن:

Prelude> max 7 8
8
Prelude> min 10 (-10)
-10
Prelude> max (3, 4) (2, 3)
(3,4)
Prelude> min [2, 3, 4, 5] [3, 4, 5, 6]
[2,3,4,5]
Prelude> max "Julie" "Chris"
"Julie"

از روی تایپ سیگنچر ِ این تابع‌ها مشخصه که دو تا پارامتر دارن. اگه بخواین بیشترین یا کمترین بین سه مقدار رو پیدا کنین، میشه دو تا از اون توابع رو تودرتو کرد:

Prelude> max 7 (max 8 9)
9

اگه فقط یه آرگومان بدین، این پیغامِ خطایِ در ظاهر عجیب رو می‌بینین:

Prelude> max "Julie"

No instance for (Show ([Char] -> [Char]))
--    [1]        [2]   [       3      ]
arising from a use of ‘print’
--                      [4]
In a stmt of an interactive GHCi command: print it
--              [               5                ]

۱.

هسکل برای یه مقدار از یه تاپی، یک نمونه ِ تایپکلاسی رو پیدا نکرد.

۲.

نمونه ای که پیدا نکرد از تایپکلاسِ ‏‎Show‎‏ بود (که به GHCi اجازه‌ی چاپ میده). این تایپکلاس رو جلوتر می‌بینیم.

۳.

برای تایپِ ‏‎String -> String‎‏ یه نمونه از ‏‎Show‎‏ پیدا نکرد. به عنوانِ یه قاعده‌ی کلی، هیچ چیز با تایپِ ‏‎(->)‎‏ نباید از ‏‎Show‎‏ نمونه داشته باشه چون ‏‎(->)‎‏ یه تابع رو نشون میده و نه یه مقدارِ ثابت.

۴.

چون (به طور غیرمستقیم) تابعِ ‏‎print‎‏ رو صدا زدیم، یه نمونه از ‏‎Show‎‏ لازم داشتیم. به محدودیت تایپکلاسی ِ ‏‎Show‎‏ در تایپ‌ش دقت کنین: ‏‎print :: Show a => a -> IO ()‎‏

۵.

دستورِ ‏‎print it‎‏ از GHCi، از طرف ما تابعِ ‏‎print‎‏ رو صدا زد.

هر وقت از GHCi درخواستِ چاپ به ترمینال می‌کنیم، در واقع داریم غیرمستقیم تابعِ ‏‎print‎‏ با تایپِ ‏‎Show a => a -> IO ()‎‏ رو صدا می‌زنیم. آرگومانِ اولِ ‏‎print‎‏ باید یه نمونه از ‏‎Show‎‏ داشته باشه. اون پیغامِ خطا به خاطر کم بودنِ آرگومان‌های ‏‎max‎‏ بود، یعنی باید یه ‏‎String‎‏ ِ دیگه بهش بدیم تا یه ‏‎String‎‏ (یا همون ‏‎[Char]‎‏) رو برگردونه که تایپی با تایپکلاسِ ‏‎Show‎‏ ِه. تا قبل از اعمالِ این آرگومانِ دوم، هنوز یه تابع‌ه، و یه تابع، نمونه ای از ‏‎Show‎‏ نداره. علت اون خطا درخواستِ چاپ ِ یه تابع بجای یه مقدار بود.

نمونه‌های ‏‎Ord‎‏

همینطور که پیش میریم مثال‌های بیشتری از نمونه نوشتن می‌بینیم و با جزئیاتِ بیشتری نحوه‌ی تعریفِ نوع‌داده‌هایِ خودتون رو توضیح میدیم. قبل‌تر چندتا نمونه برای ‏‎Eq‎‏ نوشتیم. حالا با نوشتن نمونه‌های ‏‎Ord‎‏ مهارتِ نمونه‌نویسی (که از مهارت‌های واجب در هسکل‌ه) رو در خودمون تقویت می‌کنیم.

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

data DayOfWeek =
  Mon | Tue | Weds | Thu | Fri | Sat | Sun
  deriving (Ord, Show)

فقط ‏‎Ord‎‏ و ‏‎Show‎‏ رو مشتق گرفتیم چون باید هنوز نمونه ِ ‏‎Eq‎‏ که برای این نوع‌داده نوشتیم رو در گستره داشته باشین. اگه ندارین، دو چاره دارین: یکی اینکه بیارین‌ش توی این فایلی که الان دارین می‌نویسین، یا ‏‎Eq‎‏ رو داخلِ پرانتز تایپکلاس‌هایی که مشتق گرفتین اضافه کنین. بدونِ ‏‎Eq‎‏ نمیشه تایپکلاسِ ‏‎Ord‎‏ داشت. پس اگه یه کدوم از اون کارها رو انجام ندین (فقط یکی، نه هر دو) کامپایلر بهِتون گیر میده.

مقادیر سمت چپ کوچکتر از مقادیر سمت راست به حساب میان؛ شبیه این می‌مونه که روی یه نمودارِ اعداد نوشته شده باشن:

Prelude> Mon > Tue
False
Prelude> Sun > Mon
True
Prelude> compare Tue Weds
LT

ولی اگه بخوایم بگیم جمعه همیشه بهترین روز هفته‌ست، می‌تونیم خودمون نمونه ِ ‏‎Ord‎‏ رو بنویسیم:

data DayOfWeek =
  Mon | Tue | Weds | Thu | Fri | Sat | Sun
  deriving (Eq, Show)

instance Ord DayOfWeek where
  compare Fri Fri = EQ
  compare Fri _   = GT
  compare _ Fri   = LT
  compare _ _     = EQ

حالا اگه جمعه رو با بقیه‌ی روزها مقایسه کنیم، همیشه بزرگتره. دقت کنین که ارزش بقیه‌ی روزها یکسانه:

Prelude> compare Fri Sat
GT
Prelude> compare Sat Mon
EQ
Prelude> compare Fri Mon
GT
Prelude> compare Sat Fri
LT
Prelude> Mon > Fri
False
Prelude> Fri > Sat
True

ولی تایپکلاسِ ‏‎Eq‎‏ رو بالا مشتق گرفتیم، پس رفتارِ تساوی همونطور که انتظار داریم میشه:

Prelude> Sat == Mon
False
Prelude> Fri == Fri
True

موقعِ نوشتن نمونه‌های ‏‎Ord‎‏ به چندتا چیز دقت کنین: اول اینکه هماهنگیِ ‏‎Ord‎‏ با نمونه ِ ‏‎Eq‎‏ (چه مشتق گرفته شده باشه، چه خودتون نوشته باشین) کار عاقلانه‌ایه. اگه ‏‎x == y‎‏، اون موقع باید جوابِ ‏‎compare x y‎‏ هم ‏‎EQ‎‏ باشه. دیگه اینکه نمونه‌تون یه ترتیبِ معقول از مقادیر رو توصیف کنه. یعنی همه‌ی حالت‌ها رو در نظر بگیرین و نمونه ِ ناقص ننویسین (همونطور که سَرِ ‏‎Eq‎‏ توضیح دادیم). در کل، نمونه ِ ‏‎Ord‎‏ ِتون باید طوری نوشته بشه که اگه ‏‎compare x y‎‏ جواب ‏‎LT‎‏ میده، ‏‎compare y x‎‏ خروجی ‏‎GT‎‏ بده.

‏‎Ord‎‏ تایپکلاسِ ‏‎Eq‎‏ رو نتیجه میده

به دلایلی که قبلاً گفتیم، کدِ زیر تایپچک نمیشه:

check' :: a -> a -> Bool
check' a a' = a == a'

پیغام خطا میگه ‏‎Eq‎‏ لازم داریم، که با عقل جور در میاد!

No instance for (Eq a) arising from a use of ‘==’
Possible fix:
  add (Eq a) to the context of
    the type signature for check' :: a -> a -> Bool
In the expression: a == a'
In an equation for ‘check'’: check' a a' = a == a'

چی میشه اگه بجای ‏‎Eq‎‏ که خواسته، ‏‎Ord‎‏ اضافه کنیم؟

check' :: Ord a => a -> a -> Bool
check' a a' = a == a'

کامپایل میشه. GHC تایپکلاسِ ‏‎Ord‎‏ نخواسته بود، پس چرا کار کرد؟ چون طبق تعریف، هر چیزی که یه نمونه از ‏‎Ord‎‏ داره، باید یه نمونه از ‏‎Eq‎‏ هم داشته باشه. از کجا می‌دونیم؟ بالاتر دلیل مفهومی‌ش رو گفتیم – که برای مرتب کردنِ مقادیر، بررسیِ تساوی‌شون لازمه. اما می‌تونیم نتیجه‌ی ‏‎:info Ord‎‏ توی GHCi هم ببینیم:

Prelude> :info Ord
class Eq a => Ord a where

... با یه مشت سروصدا ...

تعریفِ کلاس ‏‎Ord‎‏ میگه هر ‏‎a‎‏ ای که بخواد یه نمونه از ‏‎Ord‎‏ تعریف کنه، حتماً باید یه نمونه از ‏‎Eq‎‏ هم داشته باشه. در واقع ‏‎Eq‎‏ یه اَبرکلاس برای ‏‎Ord‎‏ ِه.

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

تمرین‌ها: کار می‌کنه؟

بگین کُدهای زیر کار می‌کنن یا نه، اگه کار می‌کنن چه جوابی میدن، و اگه کار نمی‌کنن دلیل‌شون چیه (طبق معمول جواب‌هاتون رو تو REPL تست کنین):

۱.

max (length [1, 2, 3])
    (length [8, 9, 10, 11, 12])

۲.

compare (3 * 4) (3 * 5)

۳.

compare "Julie" True

۴.

(5 + 3) > (3 + 6)