۱۰ - ۶نحوه‌ی نوشتن توابع فولد

برای نوشتن فولدها، باید اول از همه به اینکه مقدارِ اولیه‌مون چی باشه فکر کنیم. معمولاً این مقدار برابر با مقدار همانی ِ تابعِ فولدینگ ِه. مثلاً در جمع کردنِ المان‌های لیست، با صفر (مقدار همانی برای جمع) شروع می‌کنیم. وقتی المان‌های لیست رو با هم ضرب می‌کنیم، مقدار همانی ۱ میشه. این مقدارِ اولیه، مقدار پشتیبان ِ فولد هم هست: برای زمانی که لیست خالی‌ه.

بعد آرگومان‌هامون رو در نظر می‌گیریم. یه تابع فولدینگ دو آرگومان می‌گیره، یکی ‏‎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