۶ - ۵نوشتن نمونهی تایپکلاس
هنوز از نوشتن نوعدادهها یا تایپکلاسهای خودتون چیزِ زیادی نگفتیم؛ ولی هر دو کار رو میتونین انجام بدین، انجام هم میدین. در هر دو مورد پیش میاد که لازم بشه نمونههای تایپکلاسی رو خودتون بنویسین. با اینکه تایپکلاسِ Eq
رو میشه خیلی راحت مشتق گرفت، ولی چون نوشتن نمونههاش خیلی سادهست، اینجا برای نشون دادنِ طرز نوشتن نمونههای تایپکلاسها، خودمون مینویسیمشون.
نمونههای Eq
همونطور که دیدیم، Eq
نمونههایی برای تشخیص تساوی ِ مقادیر ارائه میده، ساختنِ یه نمونه از اون هم برای یه نوعداده کارِ سادهایه.
با مراجعه به راهنمای یه تایپکلاس در Hackage میتونین اون تایپکلاس رو بیشتر بررسی کنین. تایپکلاسی مثل Eq
از کتابخونه ِ base
ِه، که اینجاست. آدرس دقیقتر برای تایپکلاسِ Eq
اینه.
در اون راهنما به یه جملهی خاص دقت کنین:
Minimal complete definition: either == or /=.
./= حداقل تعاریفِ لازم برای کامل بودن: == یا
این جمله به شما متودهایی که باید تعریف کنین تا یه نمونه ِ معتبر از Eq
داشته باشین رو میگه. در این مورد یا (==)
(تساوی) یا (/=)
(نامساوی) کافیاند، چرا که یکیشون برعکس اون یکیه. چرا فقط (==)
نه؟ کم پیش میاد ولی ممکنه برای یه نوعداده ِ خاص، برای هر تابع بتونین با زِرَنگی کُدِ سریعتری بنویسین، به همین خاطر امکانش هست که هر دوشون رو تأمین کنین. اینجا ما با نوعداده ِ خاصی کار نمیکنیم و یکیشون کفایت میکنه.
اول با یه نوعداده ِ پیش و پا افتاده و کوچولو کار میکنیم... به اسم Trivial
!
data Trivial =
Trivial
بدون عبارتِ deriving
چسبیده به دُمِ این تعریفِ نوعداده، هیچ جور نمونه ِ تایپکلاسیای نداریم. اگه همینجوری بخوایم برای این داده تساوی رو بررسی کنیم، GHCi خطای تایپ میده:
Prelude> Trivial == Trivial
No instance for (Eq Trivial) arising
from a use of ‘==’
In the expression Trivial == Trivial
In an equation for ‘it’: it = Trivial == Trivial
GHC نمیتونه یه نمونه از Eq
برای نوعداده ِمون Trivial
پیدا کنه. هم میتونستیم به GHC بگیم برامون بنویسه، هم میشد خودمون بنویسیم، ولی هیچ چیز براش ننوشتیم و در زمان کامپایل به مشکل میخوره. در بعضی زبانها چنین اشتباهاتی وقتی معلوم میشن که برنامه وسط اجراشه.
برخلاف بقیهی زبانها، هسکل اصلاً نوشتهکننده (Show
یا چاپ) یا تساوی (Eq
، چه تساویِ مقدار چه تساویِ اشارهگر) ِ عمومی اختصاص نمیده، چرا که همیشه عاقلانه و امن نیست، تو هر زبانی.
پس خودمون باید بنویسیم! خوشبختانه برای Trivial
کارِ پیشوپا افتادهایه*. همیشه نمونههای تایپکلاسیِ یه تایپ رو داخل همون فایلی که تایپ توش هست تعریف کنین (دلیلش رو بعداً میگیم):
data Trivial =
Trivial'
instance Eq Trivial where
Trivial' == Trivial' = True
م. لغت trivial به معنای پیشوپا افتادهست.
همین! یه نمونه نوشتیم که به کامپایلر طریقهی بررسیِ تساوی برای این نوعداده رو میگه. دادهسازها و نوعسازها در هسکل معمولاً همنام هستن که ممکنه یه کم گیجکننده باشه. اون پریم (م. یا پرایم) رو آخرِ دادهساز ِمون نوشتیم که هم دنبال کردن مثال راحتتر بشه، هم اینکه نشون بدیم لازم نیست یکی باشن.
اگه این رو بارگذاری کنین، فقط یه بیانیه هست که بتونین بنویسین:
Prelude> Trivial' == Trivial'
True
جزبهجزء اون نمونه رو بررسی میکنیم:
instance Eq Trivial where
-- [1] [2] [3] [4]
Trivial' == Trivial' = True
-- [5] [6] [7] [8]
instance Eq Trivial where
(==) Trivial' Trivial' = True
-- [ 9 ]
۱.
کلیدواژه ِ instance
شروعِ یه نمونه ِ تایپکلاس رو مشخص میکنه. نمونه ِ تایپکلاسها برای این هستن که یه سری کارها مثلِ نوشتهکننده (Show
)، مرتبپذیری (Ord
)، شمارش (Enum
) و غیره رو برای یه نوعداده ِ خاص تعریف کنیم. بدون این نمونه نمیتونیم مقادیر رو برای تساوی بررسی کنیم. البته با این نوعداده ِ بخصوص جواب همیشه یه چیزه.
۲.
اولین اسم بعد از instance
، تایپکلاسیه که این نمونه میخواد تأمین کنه، اینجا Eq
.
۳.
اون تایپی که داریم براش نمونه مینویسیم. در این مورد داریم تایپکلاسِ Eq
رو برای نوعداده ِ Trivial
تعریف میکنیم.
۴.
کلیدواژه ِ where
، تعاریف اولیه و شروعِ نمونه رو تموم میکنه. چیزهایی که بعدش میان متودها یا توابعی هستن که میخوایم تعریف کنیم.
۵.
دادهساز (یا مقدار) ِ Trivial'
اولین آرگومانیه که برای تابعِ ==
تأمین میکنیم. اینجا از ==
به صورت میانوند استفاده کردیم، پس آرگومانِ اولش سمت چپ میاد.
۶.
تابعِ میانوند ِ ==
، این چیزیه که داریم تعریف میکنیم.
۷.
آرگومانِ دوم که مقدارش Trivial'
ِه. باز هم به خاطرِ میانوند بودنِ ==
، آرگومانِ دوم سمت راست میاد.
۸.
نتیجهی Trivial' == Trivial'
که True
هست.
۹.
تابعِ (==)
رو، به کمکِ پرانتز میشد پیشوندی هم تعریف کنیم. دقت کنید که این رو فقط برای نشون دادن نوشتیم؛ نمیشه دو تا نمونه از یه تایپکلاس برای یه تایپ داشته باشیم. نمونههای تایپکلاس در یه تایپ یکتا اند. میتونین امتحان کنین و هر دوی اینها رو تو یه فایل بنویسین، ولی خطا میگیرین.
خیلی خب، بریم سراغِ یه چیزی که کمتر پیشوپا افتاده
باشه! نوعدادههامون رو خودمون مینویسیم – یکی برای روزهای هفته و یکی هم برای تاریخ که از تایپِ DayOfWeek
استفاده میکنه:
data DayOfWeek =
Mon | Tue | Weds | Thu | Fri | Sat | Sun
-- روز هفته و روز عددی ماه
data Date =
Date DayOfWeek Int
چون اینها در هسکل پیشساخته نیستن، هیچ تایپکلاسی هم ندارن. و چون هیچ عملیاتای براشون تعریف نشده، همینطوری به خودیِخود هیچ کاری نمیشه باهاشون کرد. بیاین درستش کنیم. اولین نمونه ِ Eq
رو برای DayOfWeek
مینویسیم (با اندکی مشقت!):
instance Eq DayOfWeek where
(==) Mon Mon = True
(==) Tue Tue = True
(==) Weds Weds = True
(==) Thu Thu = True
(==) Fri Fri = True
(==) Sat Sat = True
(==) Sun Sun = True
(==) _ _ = False
حالا نمونه ِ Eq
برای Date
رو مینویسیم. این جذابتره:
instance Eq Date where
(==) (Date weekday dayOfMonth)
(Date weekday' dayOfMonth') =
weekday == weekday'
&& dayOfMonth == dayOfMonth'
برای این نمونه، طرز کارِ تساوی ِ DayOfWeek
و اعداد رو دوباره تعریف نکردیم؛ فقط گفتیم که تساوی ِ محتویاتِ Date
برای تساوی ِ خودِ تاریخ کفایت میکنه. کامپایلر خودش انتظار داره که آرگومانهای Date
از تایپهای DayOfWeek
و Int
باشن و لازم نیست مشخص کنیم. همین اطلاعاتی که تأمین کردیم برای بررسی تساوی ِ دو مقدارِ Date
کافیاند.
کار میکنه؟
Prelude> Date Thu 10 == Date Thu 10
True
Prelude> Date Thu 10 == Date Thu 11
False
Prelude> Date Thu 10 == Date Weds 10
False
کامپایل شد و در هر سه مورد کار کرد!
به یه چیزِ دیگه از این تایپها اشاره میکنیم:
Prelude> Date Thu 10
<interactive>:26:1:
No instance for (Show Date)
arising from a use of ‘print’
In a stmt of an interactive GHCi command:
print it
با نمونه ِ Eq
که نوشتیم، تونستیم تساوی ِ مقادیرش رو چک کنیم، ولی چون نمونهای از Show
براش ننوشتیم هنوز نمیتونیم تو REPL چاپ ِشون کنیم. اگه دوست دارین درستش کنین، یه عبارتِ deriving Show
تَهِ هر کدوم از نوعدادههای بالا اضافه کنین.
توابعِ ناقص – خطر غریب آشنا
قبلتر اعمالِ ناقص ِ توابع رو گفتیم ولی تابعِ ناقص چیز دیگهایه. تابعِ ناقص تابعیه که همهی حالتها براش در نظر گرفته نشده، یعنی سناریوهایی هستن که هیچ کُدی برای محاسبهشون تعریف نکردیم.
به طور کلی باید حواسمون رو جمع کنیم و از توابعِ ناقص پرهیز کنیم، ولی باید دقتِمون رو برای تایپهایی که مثلِ DayOfWeek
چند حالت دارن دوچندان کنیم. اگه در تعریفِ نمونه ِ Eq
چنین اشتباهی میکردیم چی میشد؟
ata DayOfWeek =
Mon | Tue | Weds | Thu | Fri | Sat | Sun
instance Eq DayOfWeek where
(==) Mon Mon = True
(==) Tue Tue = True
(==) Weds Weds = True
(==) Thu Thu = True
(==) Fri Fri = True
(==) Sat Sat = True
(==) Sun Sun = True
حالتِ بیقیدوشرط رو فراموش کردیم. اگه آرگومانها فرق داشته باشن چی؟ تا وقتی یکی باشن همه چیز به نظر خوب میاد، اما به محض این که آرگومانهای متفاوت بهش بدیم، تو صورتمون میترکه:
Prelude> Mon == Mon
True
Prelud> Mon == Tue
*** Exception: code/derivingInstances.hs:
(19,3)-(25,23):
Non-exhaustive patterns in function ==
زرشک...! هسکل رو شروع کردیم که برنامههامون وسط کار نترکن. قضیه چیه؟
خبر خوب اینکه یه کار میشه کرد تا GHC بیشتر راهنماییتون کنه. اگه با دستورِ -Wall
همهی هشدارها رو تو REPL (یا تو تنظیماتِ ساخت) روشن کنیم، اون موقع هر وقت حالتی رو در نظر نگرفته باشیم GHC بهمون میگه:
Prelude> :set -Wall
Prelude> :l code/derivingInstances.hs
[1 of 1] Compiling DerivingInstances
code/derivingInstances.hs:19:3: Warning:
Pattern match(es) are non-exhaustive
In an equation for ‘==’:
Patterns not matched:
Mon Tue
Mon Weds
Mon Thu
Mon Fri
...
Ok, modules loaded: DerivingInstances.
اگه نمونه ِتون رو درست کنین و اون حالتی که False
برمیگرونه رو تأمین کنین، دیگه به خاطر الگوهای غیرفراگیر بهتون گیر نمیده.
توابعِ ناقص فقط به نمونههای تایپکلاسها محدود نمیشن. فصل بعد بیشتر صحبت میکنیم، اما دقت به کامل بودنِ خودِ توابع هم خیلی مهمه، یعنی یه تابع مثل تابع زیر نباشه که هر وقت ورودی ۲ نبود بترکه:
f :: Int -> Bool
f 2 = True
اگه این رو کامپایل کنین دوباره هشدار میگیرین (با فرضِ اینکه هنوز -Wall
رو روشن دارین). در این مورد چون Int
مقادیرِ خیلی زیادی داره، اون پیغام از یه نوشتاری استفاده میکنه که بهتون میگه مقادیری که ۲ نیستن رو در نظر نگرفتین:
Pattern match(es) are non-exhaustive
In an equation for ‘f’:
Patterns not matched:
GHC.Types.I# #x with #x `notElem` [2#]
اگه یه مقدار دیگه رو به تابعمون اضافه کنیم، اون مقدار هم به لیستِ مقادیری که در نظر گرفتین اضافه میشه:
f :: Int -> Bool
f 1 = True
f 2 = True
Pattern match(es) are non-exhaustive
In an equation for ‘f’:
Patterns not matched:
GHC.Types.I# #x with #x `notElem` [1#, 2#]
f :: Int -> Bool
f 1 = True
f 2 = True
f 3 = True
attern match(es) are non-exhaustive
In an equation for ‘f’:
Patterns not matched:
GHC.Types.I# #x with #x `notElem` [1#, 2#, 3#]
همینطور ادامه پیدا میکنه. جواب کار اینه که یه حالتِ بیقیدوشرط برای تابع تعریف کنیم که با همه چیز جور میشه. مثال زیر بدونِ گلایه کامپایل میشه و دیگه ناقص نیست:
f :: Int -> Bool
f 1 = True
f 2 = True
f 3 = True
f _ = False
یه چارهی دیگه اینه که اگه فقط چند حالت برای بررسی دارین از یه نوعدادهای استفاده کنین که مثل Int
گُنده نیست.
-- بیشوخی خیلی گُندهست
Prelude> minBound :: Int
-9223372036854775808
Prelude> maxBound :: Int
9223372036854775807
اگه میخواین دادهتون فقط چندتا حالت داشته باشه، اون رو تو یه تایپِ جمع مثل DayOfWeek
که قبلتر نشون دادیم بنویسین. مثل اکثر برنامهنویسهای زبان C از Int
به عنوان یه تایپِ جمع ِ ضمنی استفاده نکنین.
گاهی اوقات باید بیشتر بخوایم
در نوشتن نمونهای مثل Eq
برای تایپهای با پارامترهای پلیمورفیک (مثل Identity
در پایین)، گاهی پیش میاد که آرگومانهای اون هم ملزوم به داشتن یه تایپکلاسهایی باشن تا نمونههایی رو در اختیار ما بذارن که بتونیم به کمکشون نمونه ِ تایپکلاس مورد نظر رو برای نوعدادهای که شاملِ اون آرگومانها هست، بنویسیم:
data Identity a =
Identity a
instance Eq (Identity a) where
(==) (Identity v) (Identity v') = v == v'
ما اینجا میخوایم به نمونه ِ Eq
ِ آرگومانِ Identity
(که در تعریفِ داده با a
و در توصیفِ نمونه با v
نشون دادیم) اتکا کنیم. ولی این تعریف یه اشکالی داره:
No instance for (Eq a) arising from a use of ‘==’
Possible fix: add (Eq a) to the
context of the instance declaration
In the expression: v == v'
In an equation for ‘==’:
(==) (Identity v) (Identity v') = v == v'
In the instance declaration for
‘Eq (Identity a)’
مشکل اینجاست که v
و v'
هر دو از تایپ a
هستن، و ما هیچ چیز از a
نمیدونیم؛ پس نمیتونیم فرض کنیم یه نمونه از Eq
دارن. ولی میتونیم از همون گرامرای که برای محدودیت تایپکلاسی در توابع استفاده میکردیم در تعریفِ نمونهمون هم استفاده کنیم:
instance Eq a => Eq (Identity a) where
(==) (Identity v) (Identity v') = v == v'
حالا دیگه میدونیم a باید یه نمونه از Eq
داشته باشه و همه چیز درسته. با این کار هسکل در زمانِ کامپایل جلوی بررسیِ تساوی با مقادیری که از Eq
نمونه ندارن رو هم میگیره:
Prelude> Identity NoEqInst == Identity NoEqInst
No instance for (Eq NoEqInst)
arising from a use of ‘==’
In the expression:
Identity NoEqInst == Identity NoEqInst
In an equation for ‘it’:
it = Identity NoEqInst == Identity NoEqInst
اگر هم بخوایم میتونیم بیشتر از نیازمون تایپکلاس تعیین کنیم، مثل زیر که برای a
یه نمونه از تایپکلاس Ord
رو اجبار کردیم؛ در حالی که Eq
کمتر از Ord
لازم داره و اینجا جواب نیاز ما رو میده:
instance Ord a => Eq (Identity a) where
(==) (Identity v) (Identity v') =
compare v v' = EQ
این کامپایل میشه، ولی اصلاً واضح نیست چرا کسی چنین چیزی بخواد... شاید دلایل رازآلود خودش رو داره...
تمرینها: نمونههای Eq
برای این نوعدادهها نمونه ِ Eq
بنویسین.
۱.
data TisAnInteger =
TisAn Integer
۲.
data TwoIntegers =
Two Integer Integer
۳.
data StringOrInt =
TisAnInt Int
| TisAString String
۴.
data Pair a =
Pair a a
۵.
data Tuple a b =
Tuple a b
۶.
data Which a =
ThisOne a
| ThatOne a
۷.
data EitherOr a b =
Hello a
| Goodbye b