۱۰ - ۶نحوهی نوشتن توابع فولد
برای نوشتن فولدها، باید اول از همه به اینکه مقدارِ اولیهمون چی باشه فکر کنیم. معمولاً این مقدار برابر با مقدار همانی ِ تابعِ فولدینگ ِه. مثلاً در جمع کردنِ المانهای لیست، با صفر (مقدار همانی برای جمع) شروع میکنیم. وقتی المانهای لیست رو با هم ضرب میکنیم، مقدار همانی ۱ میشه. این مقدارِ اولیه، مقدار پشتیبان ِ فولد هم هست: برای زمانی که لیست خالیه.
بعد آرگومانهامون رو در نظر میگیریم. یه تابع فولدینگ دو آرگومان میگیره، یکی a
که همیشه یکی از المانهای لیسته، و یکی هم b
که یا مقدار اولیهست، یا مقدارِ انباشته شده که با پیشروی در پردازشِ لیست بدست میاد.
تابعی رو فرض کنیم که سه حرف اولِ هر مقدارِ String
در یه لیست از نوشته رو میگیره، و نتیجهش رو با یه String
ِ نهایی الحاق میده. تایپِ فولد از راست برای لیستها رو اینجا نوشتیم:
foldr :: (a -> b -> b) -> b -> [a] -> b
با نوشتنِ بخشهای اولیهی تابع شروع میکنیم:
foldr (\a b -> undefined) []
["Pizza", "Apple", "Banana"]
اینجا از یه لیست خالی برای مقدار اولیهمون استفاده کردیم. ولی چون میدونیم مقدار نهایی قراره String
باشه، با یه تغییرِ گرامری ِ کوچیک، نیّتمون رو صریحتر بیان میکنیم:
foldr (\a b -> undefined) ""
["Pizza", "Apple", "Banana"]
البته String
لیسته و این دو با هم برابرند:
Prelude> "" == []
True
اما ""
از طریقِ تایپش، نیّتِ استفادهش رو گویاتر بیان میکنه:
Prelude> :t ""
"" :: [Char]
Prelude> :t []
[] :: [t]
در مرحلهی بعد برای نوشتنِ فولد، تابعش رو مینویسیم. قبلاً یاد گرفتیم چطور سه المانِ اول از یه لیست رو بگیریم؛ اینجا هم برای String
از همون راه میریم:
foldr (\a b -> take 3 a) ""
["Pizza", "Apple", "Banana"]
خوب این تایپچک میشه و کار هم میکنه، اما نه اون کاری که مدنظر ما بود:
Prelude> let pab = ["Pizza", "Apple", "Banana"]
Prelude> foldr (\a b -> take 3 a) "" pab
"Piz"
Prelude> foldl (\a b -> take 3 a) "" pab
"Ban"
فقط سه حرف اولِ نوشته ِ اول یا آخر رو میگیریم (بسته به اینکه از چپ یا راست فولد کنیم). تفاوت ترتیب آرگومانها به خاطر تایپهای متفاوتِ foldr
و foldl
رو به یاد بیارین:
foldr :: (a -> b -> b) -> b -> [a] -> b
foldl :: (b -> a -> b) -> b -> [a] -> b
الان مشکل اینه که اصلاً لیست رو فولد نمیکنیم. فقط داریم take 3
رو روی لیست نگاشت میدیم، و المانِ اول یا المانِ آخرش رو برمیگردونیم:
Prelude> map (take 3) pab
["Piz","App","Ban"]
Prelude> head $ map (take 3) pab
"Piz"
Prelude> last $ map (take 3) pab
"Ban"
میشه با استفاده از b
و انباشته کردنِ مقادیر، این فولد رو درست کنیم. یادتون باشه که b
مقدارِ اولیهست. اگه به کُدِ بالا concat
رو اضافه کنیم به جوابی که بخوایم میرسیم:
Prelude> concat $ map (take 3) pab
"PizAppBan"
Prelude> let rpab = reverse pab
Prelude> last $ map (take 3) rpab
"BanAppPiz"
ولی ما دنبال دنبال بهانه میگردیم تا با foldr
و foldl
بازی کنیم... پس فرض میکنیم کُدِ بالا رو ندیدیم!
Prelude> let f = (\a b -> take 3 a ++ b)
Prelude> foldr f "" pab
"PizAppBan"
Prelude> let f' = (\b a -> take 3 a ++ b)
Prelude> foldl f' "" pab
"BanAppPiz"
اینجا سه حرفی که از المانهای String
در لیستِ ورودی گرفتیم رو، به اولِ نوشته ای که انباشته میکنیم الحاق دادیم. اگه بخوایم کُدمون صراحت بیشتری داشته باشه، میتونیم تایپِ مقادیر رو هم اعلام کنیم:
Prelude> :{
*Prelude| let f a b = take 3
*Prelude| (a :: String) ++
*Prelude| (b :: String)
*Prelude| :}
Prelude> foldr f "" pab
"PizAppBan"
اگه تایپ رو اشتباه اعلام کنیم، تایپچکر گیرِمون میندازه:
Prelude> :{
*Prelude| let f a b = take 3 (a :: String)
*Prelude| ++ (b :: [String])
*Prelude| :}
<interactive>:12:42
Couldn't match type ‘Char’ with ‘[Char]’
Expected type: [String]
Actual type: [Char]
In the second argument of ‘(++)’,
namely ‘(b :: [String])’
In the expression:
take 3 (a :: String) ++ (b :: [String])
میشه از چنین چیزی برای چک کردنِ اینکه چقدر کُد رو دقیق درک کردیم استفاده کنیم.
تمرینها: پردازشِ پایگاه داده
برای پردازش این داده، توابع زیر رو تعریف کنین.
import Data.Time
data DatabaseItem = DbString String
| DbNumber Integer
| DbDate UTCTime
deriving (Eq, Ord, Show)
theDatabase :: [DatabaseItem]
theDatabase =
[ DbData (UTCTime
(fromGregorian 1911 5 1)
(secondsToDiffTime 34123))
, DbNumber 9001
, DbString "Hello, world!"
, DbDate (UTCTime
(fromGregorian 1921 5 1)
(secondsToDiffTime 34123))
]
۱.
تابعی بنویسین که مقادیرِ DbDate
رو فیلتر کنه، و مقادیرِ UTCTime
ِ داخلشون رو در یه لیست برگردونه.
filterDbDate :: [DatabaseItem]
-> [UTCTime]
filterDbDate = undefined
۲.
تابعی بنویسین که مقادیرِ DbNumber
رو فیلتر کنه، و مقادیرِ Integer
ِ داخلشون رو در یه لیست برگردونه.
filterDbNumber :: [DatabaseItem]
-> [Integer]
filterDbNumber = undefined
۳.
تابعی بنویسین که جدیدترین تاریخ رو بگیره.
mostRecent :: [DatabaseItem]
-> UTCTime
mostRecent = undefined
۴.
تابعی بنویسین که همهی مقادیرِ DbNumber
رو با هم جمع کنه.
sumDb :: [DatabaseItem]
-> Integer
sumDb = undefined
۵.
تابعی بنویسین که میانگین ِ مقادیرِ DbNumber
رو پیدا کنه.
-- Double به Integer احتمالاً برای رسیدن از
-- .رو لازم داشته باشین fromIntegral
avgDb :: [DatabaseItem]
-> Double
avgDb = undefined