۷ - ۷گارد یا 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‎‏ چیه؟