۷ - ۴تطبیق الگو
در هسکل، تطبیق الگو از امکانات اساسی و پرکاربرده – انقدر اساسی و پرکاربرده که تا الان بدون اینکه چیزی بگیم ازش استفاده میکردیم. بهش که عادت کنین، دیگه ولش نمیکنین.
تطبیق الگو راهی برای انطباق ِ مقادیر با الگوها، یا انقیاد ِ متغیرها با مقادیره. قابل ذکره که الگو میتونه چیزهای خیلی متنوعی باشه، از متغیرهای تعریف نشده، تا لفظهای عددی و گرامر ِ لیست. همونطور که جلوتر میبینیم، تطبیق الگو با هر دادهسازای کار میکنه.
تطبیق الگو این امکان رو میده که داده رو "باز" کنین و بر مبنای محتوای افشا شدهش، رفتارهای متفاوتی رو از تابع به کار بگیرین. هنوز خیلی به این مورد نپرداختیم، اما دلیل داره که مقادیر رو با "دادهساز" (تأکید روی ساز) توصیف میکنیم. با تطبیق الگو میتونیم توابعمون رو طوری تعریف کنیم که بین دو یا چند حالت، بر اساسِ مقداری که منطبق میشه، تصمیم بگیرن.
الگوها با مقادیر، یا دادهسازها منطبق میشن، نه تایپها. تطبیق الگو یا موفقیتآمیزه، یا شکست میخوره و سعی به انطباق ِ الگو ِ بَعدی میکنه. اگه موفقیتآمیز باشه، متغیرها به مقادیرِ الگو مقیّد میشن. تطبیق الگو از چپ به راست و از بیرون به داخل پیش میره.
با اعداد هم میشه تطبیق الگو انجام داد. در مثالِ زیر، هر وقت آرگومانِ 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