۷ - ۷گارد یا guard
تا اینجا با بولیَنها و بیانیههایی مثلِ if-then-else
که بر مبنای مقادیرِ اونها بین دو خروجی تصمیم میگیرن کار کردیم. در این بخش به یه الگوی گرامری ِ دیگه به نامِ گارد نگاه میکنیم که اون هم با مقادیر واقعی برای تصمیمگیری بین دو یا چند جوابِ ممکن تصمیم میگیره.
بیانیهی if-then-else
اول بیانیهی if-then-else
که تو فصل تایپهای پایه باهاش آشنا شدیم رو یه دوره میکنیم. دقت کنین if-then-else
همون گارد نیست! این فقط یه دورهست تا به گاردها برسیم. الگوش این بود:
if < condition >
then < result if True >
else < result if False >
که شرط ِ if
یک بیانیه با جوابِ
Bool ِه. قبلاً دیدیم که چطور به کمک این بیانیه، چنین توابعی بنویسیم:
Prelude> let x = 0
Prelude> let a = "AWESOME"
Prelude> let w = "wut"
Prelude> if (x + 1 == 1) then a else w
"AWESOME"
مثالهای بعدی از گرامر ِ بلوکهای چندخطی برای بیانیهی if
استفاده میکنن:
-- نوشتاری دیگه برای مثال بالا
Prelude> let x = 0
Prelude> :{
Prelude| if (x + 1 == 1)
Prelude| then "AWESOME"
Prelude| else "wut"
Prelude| :}
"AWESOME"
اون توگذاری واجب نیست:
Prelude> let x = 0
Prelude> :{
Prelude| if (x + 1 == 1)
Prelude| then "AWESOME"
Prelude| else "wut"
Prelude| :}
"AWESOME"
در یکی از تمرینهای آخرِ فصل ۴، تابعی به نامِ myAbs
باید مینوشتین که قدرمطلق ِ یه عددِ حقیقی رو برمیگردوند. با بیانیهی if-then-else
میشد اینطور تعریفش کنین:
myAbs :: Integer -> Integer
myAbs x = if x < 0 then (-x) else x
الان این تابع رو با گاردها بازنویسی میکنیم.
نوشتن بلوکهای گارد
با گرامر ِ گارد میشه توابعِ جمعوجوری نوشت که برمبنای نتیجهی شرطهاشون، میتونن دو یا چند خروجی داشته باشن. نوشتار این گرامر رو با بازنویسیِ تابعِ myAbs
شروع میکنیم:
myAbs :: Integer -> Integer
myAbs x
| x < 0 = (-x)
| otherwise = x
دقت کنین که هر گارد علامت تساویِ خودش رو داره. در خط اول، بعد از پارامتر هم علامت تساوی نذاشتیم، چون هر حالت، در صورت موفقیت، باید بیانیهی خودش رو برگردونه. جز به جزءِ این گرامر رو با شمارهگذاری توضیح میدیم:
myAbs x
-- [1] [2]
| x < 0 = (-x)
-- [3] [4] [5] [6]
| otherwise = x
-- [7] [8] [9] [10]
۱.
اسمِ تابع، myAbs
هنوز اول میاد.
۲.
تابع یک پارامتر به اسمِ x
داره.
۳.
از اینجا فرق میکنه. بجای علامت تساوی درست بعد از معرفیِ پارامتر(ها)، میریم خط بعد و یه گارد رو با یه خطِ عمودی (|
) شروع میکنیم.
۴.
جوابِ این بیانیه تعیین میکنه که آیا این شاخه محاسبه بشه یا نه. این بیانیهی گارد که بینِ |
و =
نوشته میشه، باید به یه Bool
ساده بشه.
۵.
با علامت تساوی بیانیهای رو تعریف میکنیم که در صورتِ True
بودنِ x < 0
، تابع باید برگردونه.
۶.
بعد از =
بیانیهی (-x)
رو داریم که اگه x < 0
بود برمیگرده.
۷.
یه خط جدید و یه خطِ عمودی ِ دیگه، و شروعِ گارد ِ بعدی.
۸.
کلیدواژه ِ otherwise
اسمِ دیگهای برای True
ِه، که اینجا به عنوانِ یه حالت پشتیبان برای وقتی که x < 0
جوابِ False
بده ازش استفاده شده.
۹.
یک =
ِ دیگه که بیانیهی خروجی در صورت رسیدن به حالتِ otherwise
رو تعریف میکنه.
۱۰.
اگه x
کوچکتر از صفر نباشه، خودش رو برمیگردونیم.
ببینیم چطور کار میکنه:
Prelude> myAbs (-10)
10
Prelude> myAbs 10
10
در مثالِ اول، با یه آرگومانِ عددیِ منفی، به گارد ِ اول نگاه میکنه و میبینه که البته ۱۰- از صفر کوچکتره و True
میشه، پس نتیجهی بیانیهی (-x)
رو که اینجا ((۱۰-)-) یا ۱۰ هست رو برمیگردونه. در مثالِ دوم به گارد ِ اول نگاه میکنه و میبینه که ۱۰ با شرط جور نیست و False
میشه و به گارد ِ بعدی میره. گارد ِ otherwise
همیشه True
ِه پس x
که اینجا ۱۰ هست رو برمیگردونه. گاردها همیشه با تسلسل محاسبه میشن، پس گاردها رو به ترتیب از محدودترین تا جامعترین حالت بنویسین.
حالا یه تابعی رو ببینیم که بیشتر از دو خروجی داره، تابعی که میزانِ سدیم (Na) در خون رو تعیین میکنه. تابعی میخوایم که به اعداد نگاه کنه (این اعداد با واحد میلیاِکیوالان بر لیتر یا mEq/L هستند) و بگه که آیا میزانِ سدیم ِ خون نرمال هست یا نه:
bloodNa :: Integer -> String
bloodNa x
| x < 135 = "too low"
| x > 145 = "too high"
| otherwise = "just right"
تا وقتی بیانیهی هر گارد به یه مقدارِ Bool
ساده بشه، هر تایپی میتونه داشته باشه. برای مثال تابعِ زیر سهتا عدد برای طول اضلاع مثلث میگیره و میگه که آیا مثلث قائم هست یا نه (با استفاده از قضیهی فیثاغورس):
-- رو به عنوان وتر مثلث در نظر گرفتیم c
isRight :: (Num a, Eq a)
=> a -> a -> a -> String
isRight a b c
| a^2 + b^2 == c^2 = "RIGHT ON"
| otherwise = "not right"
تابع زیر هم سنِ سگتون رو میگیره و میگه به سن آدمها چند سالشه:
dogYrs :: Integer -> Integer
dogYrs x
| x <= 0 = 0
| x <= 1 = x * 15
| x <= 2 = x * 12
| x <= 4 = x * 8
| otherwise = x * 6
چرا مضربهای مختلف؟ چون هاپوها زودتر از بچهها بالغ میشن، پس یه هاپوی یک ساله معادل یه بچهی شش-هفت ساله نیست (البته این تبدیل سن یه کم پیچیدهتره، مثلاً وزن سگ هم تأثیر داره... که شاید تمرین بدی نباشه!).
از تعاریفِ where
هم میشه با گاردها استفاده کرد. فرض کنیم یه تابع ساده میخواین که از روی تعداد جوابهای صحیصِ یه دانشآموز به یه تستِ صد سؤاله، نمرهش رو با مقیاسِ A تا F تعیین کنه:
avgGrade :: (Fractional a, Ord a)
=> a -> Char
avgGrade x
| y >= 0.9 = 'A'
| y >= 0.8 = 'B'
| y >= 0.7 = 'C'
| y >= 0.59 = 'D'
| y < 0.59 = 'F'
where y = x / 100
چیزی جدید نیست. میبینید که متغیرِ y
نه به عنوان پارمترِ تابع، بلکه در بلوکِ گارد، با where
تعریف شده. حالا که اونجا تعریف شده، در گستره ِ همهی گاردهای بالاترش هم قرار گرفته. تست فرضیِ ما ۱۰۰ سؤال داشت، پس هر x
ِ ورودی بر ۱۰۰ تقسیم میشه تا نمرهی حرفی رو بده.
به حذفِ otherwise
هم دقت کنین؛ میتونستیم در گارد ِ آخر ازَش استفاده کنیم، ولی اینجا از کوچکتر از
استفاده کردیم. چون همهی حالتها رو در گاردهامون در نظر گرفتیم، پس موردی نداره. ولی حواستون باشه که GHCi همیشه بهتون نمیگه که تابعِ ناقص نوشتین، اون موقع تصحیحش هم سخت میشه، پس بهتره عموماً برای گارد ِ آخر از otherwise
استفاده کنین.
البته همونطور که قبلاً هم گفتیم، میتونین با دستورِ :set -Wall
در GHCi همهی هشدارها رو روشن کنین و اون موقع اگه در برنامهتون الگوهای غیرفراگیر داشته باشین بهتون میگه.
تمرینها: دیدهبانی
۱.
احتمالاً میدونین که چرا otherwise
رو در بالاترین گارد نمینویسیم، با این حال روی تابع avgGrade
امتحان کنین ببینین چی میشه. اینطوری بنویسین واضحتر میشه: | otherwise = 'F'
. مثلاً با آرگومانهای ۹۰، ۷۵، و ۶۰ چه جوابهایی میگیرین؟
۲.
حالا اگه تابعِ avgGrade
رو همینطور که هست بنویسین فقط ترتیبِ گاردهاش رو عوض کنین چی میشه؟ تایپچک میشه؟ مثل قبل کار میکنه؟ گاردِ | y >= 0.7 = 'C'
رو جابجا کنین، بعد آرگومان ۹۰ که جوابش باید 'A'
بشه رو بهش بدین. آیا جوابش 'A'
میشه؟
۳.
تابع زیر چه چیزی برمیگردونه؟
pal xs
| xs == reverse xs = True
| otherwise = False
a)
معکوس ِ xs
وقتی True
هست
b)
True
اگه xs
واروخوانه باشه
c)
False
اگه xs
واروخوانه باشه
d)
False
وقتی xs
معکوس میشه
۴.
pal
چه تایپ آرگومانهایی میتونه بگیره؟
۵.
تایپِ تابعِ pal
چیه؟
۶.
تابعِ زیر چه چیزی برمیگردونه؟
numbers x
| x < 0 = -1
| x == 0 = 0
| x > 0 = 1
a)
مقدار آرگومانش، بعلاوه یا منهای ۱
b)
منفیِ آرگومانش
c)
یه جور مشخصه که میگه ورودیش مثبت، منفی، یا صفره
d)
زبانِ ماشین ِ باینری
۷.
تابعِ numbers
چه تایپ آرگومانهایی میتونه بگیره؟
۸.
تایپِ تابعِ numbers
چیه؟