۷ - ۴تطبیق الگو

در هسکل، تطبیق الگو از امکانات اساسی و پرکاربرده – انقدر اساسی و پرکاربرده که تا الان بدون اینکه چیزی بگیم ازش استفاده می‌کردیم. بهش که عادت کنین، دیگه ولش نمی‌کنین.

تطبیق الگو راهی برای انطباق ِ مقادیر با الگوها، یا انقیاد ِ متغیرها با مقادیره. قابل ذکره که الگو می‌تونه چیزهای خیلی متنوعی باشه، از متغیرهای تعریف نشده، تا لفظ‌های عددی و گرامر ِ لیست. همونطور که جلوتر می‌بینیم، تطبیق الگو با هر داده‌ساز‌ای کار می‌کنه.

تطبیق الگو این امکان رو میده که داده رو "باز" کنین و بر مبنای محتوای افشا شده‌ش، رفتارهای متفاوتی رو از تابع به کار بگیرین. هنوز خیلی به این مورد نپرداختیم، اما دلیل داره که مقادیر رو با "داده‌ساز" (تأکید روی ساز) توصیف می‌کنیم. با تطبیق الگو می‌تونیم توابع‌مون رو طوری تعریف کنیم که بین دو یا چند حالت، بر اساسِ مقداری که منطبق میشه، تصمیم بگیرن.

الگوها با مقادیر، یا داده‌سازها منطبق میشن، نه تایپ‌ها. تطبیق الگو یا موفقیت‌آمیزه، یا شکست می‌خوره و سعی به انطباق ِ الگو ِ بَعدی می‌کنه. اگه موفقیت‌آمیز باشه، متغیرها به مقادیرِ الگو مقیّد میشن. تطبیق الگو از چپ به راست و از بیرون به داخل پیش میره.

با اعداد هم میشه تطبیق الگو انجام داد. در مثالِ زیر، هر وقت آرگومانِ ‏‎Integer‎‏ ِ تابع برابرِ ۲ باشه، خروجی ‏‎True‎‏ میشه، و در غیر اینصورت ‏‎False‎‏:

isItTwo :: Integer -> Bool
isItTwo 2 = True
isItTwo _ = False

این تابع رو مستقیماً تو GHCi هم میشه تعریف کرد. با دستورِ ‏‎:{‎‏ یه بلوکِ کُد باز، و با ‏‎:}‎‏ بلوک بسته میشه:

Prelude> :{
*Main| let isItTwo :: Integer -> Bool
*Main|     isItTwo 2 = True
*Main|     isItTwo _ = False
*Main| :}

دقت کنید که خط تیره ‏‎_‎‏ بعد از انطباق روی ۲ اومده. خط تیره یه الگو ِ جامع‌ه که هیچ وقت شکست نمی‌خوره، شبیهِ "هر حالت دیگه" می‌مونه:

Prelude> isItTwo 2
True
Prelude> isItTwo 3
False

در نظر گرفتن همه‌ی حالت‌ها

ترتیبِ تطبیق ِ الگوها مهمّه! این نسخه از تابع همیشه ‏‎False‎‏ برمی‌گردونه، چون اول با الگو ِ "هر حالت دیگه" منطبق میشه، پس هیچ چیز به الگویی که می‌خواستین نمیرسه:

isItTwo :: Integer -> Bool
isItTwo _ = False
isItTwo 2 = True
<interactive>:9:33: Warning:
    Pattern match(es) are overlapped
--  ^------------------------------^
--  م. تطبیق(های) الگو متداخل شدن
    In an equation for ‘isItTwo’:
      isItTwo 2 = ...
Prelude> isItTwo 2
False
Prelude> isItTwo 3
False

همیشه سعی کنین الگوهاتون رو از مشخص‌ترین به جامع‌ترین مرتب کنین، بخصوص وقتی از ‏‎_‎‏ برای انطباق ِ بقیه‌ی مقادیر استفاده می‌کنین. عموماً بهتره به هشدارهای GHCi در موردِ الگوهای متداخل اعتماد و کُدتون رو چک کنین.

چی میشه اگه حالتی رو در نظر نگیریم؟

isItTwo :: Integer -> Bool
isItTwo 2 = True

الان این تابع فقط روی ۲ تطبیق الگو میده. این یه تطبیق الگو ِ ناقص ِه چون هیچ داده‌ی دیگه‌ای رو منطبق نمی‌کنه. اعمال ِ اینجور تطبیقِ الگوهای ناقص به مقادیری که در نظر نگرفتین، تهی برمی‌گردونن، یه غیر-مقدار که نشون دهنده‌ی خروجی یا نتیجه نداشتن یه برنامه‌ست. چنین تابعی استثنا میده، که اگه درست نشه، ممکنه برنامه‌تون رو از کار بندازه:

Prelude> isItTwo 2
True
Prelude> isItTwo 3
*** Exception: :50:33-48:
  Non-exhaustive patterns
--^---------------------^
-- م. الگوهای غیرفراگیر
      in function isItTwo

در فصل‌های آینده با تهی خیلی بیشتر آشنا میشیم. ولی فعلاً همین چیزهایی که گفتیم کفایت می‌کنن (نتیجه‌ی یه تابعِ ناقص).

خوشبختانه راهی برای پیدا کردنِ الگوهای غیرفراگیر که همه‌ی حالت‌ها رو در نظر نگرفتن، اون هم در لحظه‌ی کامپایل، وجود داره:

Prelude> :set -Wall
Prelude> :{
*Main| let isItTwo :: Integer -> Bool
*Main|     isItTwo 2 = True
*Main| :}

<interactive>:28:5: Warning:
    This binding for ‘isItTwo’ shadows
    the existing binding
      defined at <interactive>:20:5

<interactive>:28:5: Warning:
    Pattern match(es) are non-exhaustive
    In an equation for ‘isItTwo’:

    Patterns not matched:
      #x with #x `notElem` [2#]

با دستورِ ‏‎-Wall‎‏ همه‌ی هشدارها رو روشن کردیم و اشتباه‌مون زودتر مشخص شد. اصلاً هشدارهای GHCi رو نادیده نگیرین!

تطبیق الگو با داده‌سازها

تطبیق الگو چند قابلیت رو در اختیار میذاره. یکی اینکه اجازه میده تعریف توابع‌مون رو بسته به ورودی‌شون تغییر بدیم. و همینطور امکانِ باز و افشا کردن محتوای داده‌‌هامون رو فراهم می‌کنه. مقادیرِ ‏‎True‎‏ و ‏‎False‎‏، داده‌‌های دیگه‌ای برای افشا کردن ندارن، ولی بعضی داده‌سازها پارامتر دارن، و با تطبیق الگو میشه داده‌های درونِ آرگومان‌هاشون رو افشا و استفاده کرد.

در مثال بعدی از ‏‎newtype‎‏ که یه حالت خاص از تعریفِ ‏‎data‎‏ هست استفاده کردیم. تفاوتِ ‏‎newtype‎‏ اینه که فقط اجازه‌ی یک سازنده با یک فیلد (جا) رو میده. بعداً بیشتر از ‏‎newtype‎‏ صحبت می‌کنیم. فعلاً روی طریقه‌ی افشای محتویات داده‌‌ها به کمک تطبیق الگو، و تعریفِ تابع براساس اون داده‌‌ها تمرکز می‌کنیم:

-- registeredUser1.hs
module RegisteredUser where

newtype Username = Username String
newtype AccountNumber = AccountNumber Integer

data User = UnregisteredUser
          | RegisteredUser Username AccountNumber

با تایپِ ‏‎User‎‏، میشه از تطبیق الگو استفاده کنیم تا دو کار انجام بدیم. اول اینکه ‏‎User‎‏ یه تایپ جمع با دو داده‌ساز ِه، ‏‎UnregisteredUser‎‏ و ‏‎RegisteredUser‎‏. با استفاده از تطبیق الگو می‌تونیم تابع‌مون را بسته به هر کدوم از این مقادیر با تعریف متفاوتی خبر کنیم. داده‌ساز ِ ‏‎RegisteredUser‎‏ هم از ضرب ِ دو ‏‎newtype‎‏ درست شده، ‏‎Username‎‏ و ‏‎AccountNumber‎‏. نه تنها با تطبیق الگو می‌تونیم ‏‎RegisteredUser‎‏ رو باز کنیم، بلکه (در صورتی که همه‌ی سازنده‌ها در گستره باشن) محتوای ‏‎newtype‎‏‌ها رو هم میشه افشا کرد. در مثال زیر یه تابع برای چاپ ِ زیباتر مقادیرِ ‏‎User‎‏ تعریف می‌کنیم:

-- registeredUser2.hs
module RegisteredUser where

newtype Username = Username String
newtype AccountNumber = AccountNumber Integer

data User = UnregisteredUser
          | RegisteredUser Username AccountNumber

printUser :: User -> IO ()
printUser UnregisteredUser
          = putStrLn "UnregisterdUser"
printUser (RegisteredUser (Username name)
                          (AccountNumber acctNum))
          = putStrLn $ name ++ " " ++ show acctNum

دقت کنین که اگه الگو طولانی باشه، می‌تونیم خط بعد ادامه‌ش رو بنویسیم. حالا این فایل رو تو REPL بارگذاری و تایپ‌ها رو بررسی می‌کنیم:

Prelude> :l code/registeredUser2.hs
...
Prelude> :t RegisteredUser
RegisteredUser :: Username
               -> AccountNumber
               -> User
Prelude> :t Username
Username :: String -> Username
Prelude> :t AccountNumber
AccountNumber :: Integer -> AccountNumber

می‌بینیم که تایپِ ‏‎RegisteredUser‎‏ در واقع تابعی‌ه که با دو آرگومانِ ‏‎Username‎‏ و ‏‎AccountNumber‎‏ یه مقدارِ ‏‎User‎‏ درست می‌کنه. منظور ما از "داده‌ساز" (م. یا سازنده ِ داده) همینه.

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

Prelude> printUser UnregisteredUser
UnregisteredUser

مثال بعدی جذاب‌تره چون تطبیق الگو روی یه داده‌ساز ِ ‏‎RegisteredUser‎‏ انجام میشه:

Prelude> let myUser = Username "callen"
Prelude> let myAcct = AccountNumber 10456
Prelude> :{
*Main| let rUser =
*Main|       RegisteredUser myUser myAcct
*Main| :}
Prelude> printUser rUser
callen 10456

به کمک تطبیق الگو، مقدارِ ‏‎RegisteredUser‎‏ از تایپِ ‏‎User‎‏ رو باز کردیم و رفتار متفاوتی از تابع گرفتیم.

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

data WherePenguinsLive =
    Galapagos
  | Antarctica
  | Australia
  | SouthAfrica
  | SouthAmerica
  deriving (Eq, Show)

و یه تایپِ ضرب به اسمِ ‏‎Penguin‎‏. تا اینجا خیلی به تایپ‌های ضرب توجه نکردیم، ولی فعلاً ‏‎Penguin‎‏ رو یه تایپ با یک مقدارِ ‏‎Peng‎‏ در نظر بگیرین که یه جور جعبه‌ست، حاوی یک مقدارِ ‏‎WherePenguinsLive‎‏:

data Penguin =
  Peng WherePenguinsLive 
  deriving (Eq, Show)

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

-- برگردون True اگه آفریقای جنوبی بود
isSouthAfrica :: WherePenguinsLive -> Bool
isSouthAfrica SouthAfrica = True
isSouthAfrica Galapagos = False
isSouthAfrica Antarctica = False
isSouthAfrica Australia = False
isSouthAfrica SouthAmerica = False

زیاده‌نویسی کردیم. می‌تونستیم با ‏‎_‎‏ یه الگو ِ بدون شرط برای مقادیری که مهم نیستن استفاده کنیم. تابع زیر بهتره (خلاصه‌تر با خوانایی بیشتر) و همون کار رو انجام میده:

isSouthAfrica' :: WherePenguinsLive -> Bool
isSouthAfrica' SouthAfrica = True
isSouthAfrica' _           = False

می‌تونیم با تطبیق الگو مقادیرِ ‏‎Penguin‎‏ هم باز کنیم تا به مقدارِ ‏‎WherePenguinsLive‎‏ که داخل‌ش قرار داره دسترسی پیدا کنیم:

gimmeWhereTheyLive :: Penguin
                   -> WherePenguinsLive
gimmeWhereTheyLive (Peng whereitLives) =
  whereitlives

تابع ‏‎gimmeWhereTheyLive‎‏ رو با داده‌‌های زیر امتحان کنین (اسم چندتا پنگوئن، به حرف اول کوچک‌شون دقت کنین). وقتی اسم پنگوئن رو وارد می‌کنین، تابع مقدارِ ‏‎Peng‎‏ رو باز می‌کنه و ‏‎WherePenguinsLive‎‏ ِ داخل‌ش رو برمی‌گردونه:

humboldt = Peng SouthAmerica
gentoo = Peng Antarctica
macaroni = Peng Antarctica
little = Peng Australia
galapagos = Peng Galapagos

حالا یه مثالِ پربارتر. محتویاتِ ‏‎Peng‎‏ رو افشا می‌کنیم و در یک تطبیقِ الگو، روی مقدار ‏‎WherePenguinsLive‎‏ ای که می‌خوایم رفتار تابع رو تعریف می‌کنیم:

galapagosPenguin :: Penguin -> Bool
galapagosPenguin (Peng Galapagos) = True
galapagosPenguin _                = False

antarcticPenguin :: Penguin -> Bool
antarcticPenguin (Peng Antarctica) = True
antarcticPenguin _                 = False

در این تابعِ آخر، عملگر ِ ‏‎(||)‎‏ یه تابعِ یا هست که اگه یکی از دو ورودی‌ش ‏‎True‎‏ باشه، ‏‎True‎‏ برمی‌گردونه:

antarcticaOrGalapagos :: Penguin -> Bool
antarcticaOrGalapagos p =
     (galapagosPenguin p)
  || (antarcticPenguin p)

دقت کنین که اینجا با تطبیق الگو دو کار انجام دادیم. یکی نوع‌داده ِ ‏‎Penguin‎‏ رو باز کردیم، و یکی اینکه تعیین کردیم روی کدوم یکی از مقادیرِ ‏‎WherePenguinsLive‎‏ انطباق کنیم.

تطبیق الگو برای توپل‌‌ها

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

f :: (a, b) -> (c, d) -> ((b, d), (a, c)) 
f = undefined

احتمالاً شبیهِ این حل‌ِش کردین:

f :: (a, b) -> (c, d) -> ((b, d), (a, c)) 
f x y = ((snd x, snd y), (fst x, fst y))

ولی با تطبیق الگو می‌تونیم یه کم تمیزتر بنویسیم‌ش:

f :: (a, b) -> (c, d) -> ((b, d), (a, c)) 
f (a, b) (c, d) = ((b, d), (a, c))

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

-- matchingTuples1.hs
module TupleFunctions where

-- باید از یه تایپ باشن چون
-- (+) :: Num a => a -> a -> a
addEmUp2 :: Num a => (a, a) -> a
addEmUp2 (x, y) = x + y

-- رو اینطور هم میشه نوشت addEmUp2
addEmUp2Alt :: Num a => (a, a) -> a
addEmUp2Alt tup = (fst tup) + (snd tup)

fst3 :: (a, b, c) -> a
fst3 (x, _, _) = x

third3 :: (a, b, c) -> c
third3 (_, _, x) = x
Prelude> :l code/matchingTuples1.hs
[1 of 1] Compiling TupleFunctions
Ok, modules loaded: TupleFunctions.

با دستورِ ‏‎:browse‎‏ تو GHCi میشه یه لیست از تایپ سیگنچرها و توابعی که با ماژول ِ ‏‎TupleFunctions‎‏ بارگذاری کردیم رو ببینیم:

Prelude> :browse TupleFunctions
addEmUp2 :: Num a => (a, a) -> a
addEmUp2Alt :: Num a => (a, a) -> a
fst3 :: (a, b, c) -> a
third3 :: (a, b, c) -> c
Prelude> addEmUp2 (10, 20)
30
Prelude> addEmUp2Alt (10, 20)
30
Prelude> fst3 ("blah", 2, [])
"blah"
Prelude> fst3 ("blah", 2, [])
[]

خیلی خوب. حالا یه کم تمرین کنیم عضلات‌مون تقویت شن!

تمرین‌ها: بسته‌ی متنوع

۱.

با توجه به تعاریفِ زیر

k (x, y) = x
k1 = k ((4-1), 10)
k2 = k ("three", (1 + 2))
k3 = k (3, True)

a)

‏‎k‎‏ چه تایپی داره؟

b)

تایپِ ‏‎k2‎‏ چطور؟ آیا تایپ‌ش با ‏‎k1‎‏ و ‏‎k3‎‏ یکسانه؟

c)

بین ‏‎k1‎‏، ‏‎k2‎‏، و ‏‎k3‎‏ کدوم‌شون جوابِ ۳ میده؟

۲.

تعریف تابع زیر رو بنویسین:

-- یادتون باشه که گرامر توپل‌ها
-- برای نوع‌ساز و داده‌سازِشون یکسانه

f :: (a, b, c)
  -> (d, e, f)
  -> ((a, d), (c, f))
f = undefined