۱۰ - ۵فولد از چپ
به خاطر طرز کار لیستها، فولدها باید اول از روی کلِ ستون ِ لیست، از ابتدا تا انتها پیش برن. در فولد ِ چپ و راست، جهتِ پیمایش ِ ستون یکسانه، ولی در foldl
، فرایندِ فولد کردن شرکتپذیری از چپ داره و در خلافِ جهتِ foldr
انجام میشه.
در زیر یه تعریف ساده از
foldl رو آوردیم. باز هم مثل foldr
، تایپها ممکنه فرق داشته باشن:
foldl :: (b -> a -> b) -> b -> [a] -> b
foldl f acc [] = z
foldl f acc (x:xs) = f x (foldr f acc xs)
foldl :: (b -> a -> b) -> b -> [a] -> b
-- با لیست زیر
foldl (+) 0 (1 : 2 : 3 : [])
-- اینطوری شرکت داده میشه foldl
((0 + 1) + 2) + 3
برای دیدنِ شرکتپذیری ِ foldl
، میتونیم از همون کَلَکی که برای foldr
زَدیم استفاده کنیم:
Prelude> let f x y = concat ["(",x,"+",y,")"]
Prelude> foldl f "0" (map show [1..5])
"(((((0+1)+2)+3)+4)+5)"
میبینیم که foldl
رَوندِ سادهسازیش رو با اضافه کردن سَر ِ لیست به مقدارِ acc
(م. خلاصهی accumulator به معنای مخزن یا جمعکننده) شروع میکنه، ولی foldr
از آخرین المانِ لیست شروع میکرد.
از اسکنها هم میشه برای دیدنِ نحوهی محاسبه در فولدها استفاده کرد. اسکنها شبیه فولدها اند، با این تفاوت که در نهایت یه لیست شامل مقادیر حاصل در هر مرحلهی فولد رو برمیگردونن. اگه scanr
و scanl
رو با فولدهاشون مقایسه کنیم:
Prelude> foldr (+) 0 [1..5]
15
Prelude> scanr (+) 0 [1..5]
[15,14,12,9,5,0]
Prelude> foldl (+) 0 [1..5]
15
Prelude> scanl (+) 0 [1..5]
[0,1,3,6,10,15]
چنین رابطهای بینشون دیده میشه:
last (scanl f z xs) = foldl f z xs
head (scanr f z xs) = foldr f z xs
فولد کردن و شرکتپذیری
اینجا به چندتا از تأثیراتِ شرکتپذیری ِ foldl
نگاهِ دقیقتری میندازیم. همونطور که گفتیم، هر دو فولدها ستون رو در یک جهت پیمایش میکنن. چیزی که بینشون فرق داره، شرکتپذیری ِ محاسبهست.
نگرشِ اساسی که به محاسبات در هسکل میشه داشت، دیدنِ اونها به چشمِ جایگزینی ِه. وقتی یه لیست رو با f
و مقدار اولیهی z
از راست فولد میکنیم، میشه گفت داریم دادهسازهای cons رو با تابع فولدینگ، و دادهساز ِ "لیست خالی" رو با مقدار شروعیِ z
جایگزین میکنیم:
[1..3] = 1 : 2 : 3 : []
foldr f z [1, 2, 3]
1 `f` (foldr z [2, 3])
1 `f` (2 `f` (foldr z [3]))
1 `f` (2 `f` (3 `f` (foldr z [])))
1 `f` (2 `f` (3 `f` z))
از طرف دیگه، به خاطرِ محاسبهی تنبل اختیارِ تعیینِ ترتیبِ محاسبه به توابع داده میشه. در نتیجه اون پرانتزها واقعیاند. در مثال بالا 3 `f` z
اول محاسبه میشه، چون داخلیترین پرانتزه. فولدها لیست رو از بیرون به داخل پیمایش میکنن، ولی خودِ فولد کردن در فولد از راست، از انتهای لیست شروع میشه.
دیدن تأثیرِ چنین چیزی رو با توابعِ شرکتپذیر نمیشه دید، ولی درک این موضوع مهمه، پس ما هم چندتا مثالِ دیگه رو بررسی میکنیم. اول بریم سراغِ تابعِ توان:
Prelude> foldr (^) 2 [1..3]
1
Prelude> foldl (^) 2 [1..3]
64
الان تفاوت واضحه، که اون هم به خاطر نحوهی شرکت دادن ِ توابعه. جزبهجز بررسی کنیم:
-- اگه میخواین با کتاب پیش بیاین،
-- REPL رو کاغذ بنویسین، نه تو
foldr (^) 2 [1..3]
(1 ^ (2 ^ (3 ^ 2)))
(1 ^ (2 ^ 9))
1 ^ 512
1
با این مقایسه کنین:
foldl (^) 2 [1..3]
((2 ^ 1) ^ 2) ^ 3
(2 ^ 2) ^ 3
4 ^ 3
64
در مثال بعدی، با فولد کردن ِ یه لیست به یه لیست جدید، تأثیرِ شرکتپذیری رو نشون میدیم:
Prelude> foldr (:) [] [1..3]
[1,2,3]
Prelude> foldl (flip (:)) [] [1..3]
[3,2,1]
برای foldl
باید از flip
استفاده کنیم. ببینیم چرا.
مثل فولد از راست، فولد از چپ هم نمیتونه جادو کنه و یِهو بِره آخر لیست؛ باید از اولِ لیست شروع کنه. با این حال، پرانتزها نحوهی محاسبهی کُدمون رو تعیین میکنن. علاوه بر شرکتپذیری، تایپِ آرگومان تابعِ فولدینگ هم تغییر میکنه:
foldr :: (a -> b -> b) -> b -> [a] -> b
-- [1] [2] [3]
foldl :: (b -> a -> b) -> b -> [a] -> b
-- [4] [5] [6]
۱.
این پارامتر با تایپِ a
، آرگومانی از تابع فولدینگ رو نشون میده که از المانهای لیست تأمین میشه.
۲.
این پارامتر با تایپِ b
، بسته به این که چقدر از فولد پیش رفته، یا مقدارِ اولیه میشه، یا نتیجهای که از فولد کردن تا اون نقطه انباشته شده.
۳.
نتیجهی حاصل از اعمال ِ تابع فولدینگ به المان لیست، و مقدار اولیه یا انباشته شده.
۴.
مقدارِ اولیه یا انباشته شده در foldl
، آرگومان اولِ تابع فولدینگ میشه.
۵.
المان لیست هم آرگومانِ دومِ تابع فولدینگ در foldl
میشه.
۶.
جوابِ تابع فولدینگ ِ foldl
تایپِ b
داره، درست مثلِ foldr
.
از تایپِ (:)
میشه دید که انتظار داره آرگومانِ اولِش مقدار، و آرگومانِ دومش یه لیست باشه:
(:) :: a -> [a] -> [a]
پس مقدار ورودی، به اولِ لیست پیشافزوده میشه (یا پشتِ لیست cons میشه).
در مثالهای زیر تیلدا (م. یا مدَّک) "معادل یا برابر است با" معنی میده. با دادهساز ِ cons بجای f
و لیستِ خالی بجای z
، اگه از راست فولد کنیم:
-- foldr f z [1,2,3]
-- f ~ (:); z ~ []
-- میده. True اجرا کنین، REPL اگه در
(==) (foldr (:) [] (1 : 2 : 3 : []))
(1 : (2 : (3 : [])))
اینجا تایپ سیگنچرها با هم جور اند (تابعِ cons با تابع فولدینگ برای foldr
). حتی همون لیستِ ورودی رو جواب میده چون داریم دادهسازهای cons رو با دادهسازهای cons، و لیست خالی رو با لیست خالی جایگزین میکنیم. ولی شرکتپذیری از راست برای یکسان بودنشون واجبِه.
اگه همین کار رو با foldl
انجام بدیم، جواب همون لیستِ ورودی نمیشه. در foldl
، بجای المان لیست، مقدار انباشته شده آرگومان اول میشه. که اگه این مقدارِ انباشته شده لیست باشه، یعنی آرگومانهای (:)
رو برعکس دادیم. استفاده از (:)
به عنوانِ تابع فولدینگ ِ foldl
و لیست خالی برای انباشته کردنِ مقادیر، خطای تایپ میده:
foldl f z [1, 2, 3]
-- f ~ (:); z ~ []
-- (((z `f` 1) `f` 2) `f` 3)
((([] : 1) : 2) : 3)
الان z
یه لیستِ خالی شده و f
هم cons، که آرگومانهاش برعکساند و کار نمیکنه. تابعِ flip
جای دو تا آرگومان اولِ تابعِ ورودیش رو عوض میکنه و در نتیجه همون تابع رو با آرگومانهای جابجا شده برمیگردونه، که اینجا به کار میاد:
foldl f z [1, 2, 3]
-- f ~ (flip (:)); z ~ []
-- (((z `f` 1) `f` 2) `f` 3)
f = flip (:)
((([] `f` 1) `f` 2) `f` 3)
(([1] `f` 2) `f` 3)
([2, 1] `f` 3)
[3, 2, 1]
با اینکه تایپها رو درست کردیم، ذاتِ شرکتپذیری از چپ در foldl
منجر به یه جوابِ متفاوتی نسبت به foldr
شد.
برای سری بعدی مثالها، از تابعی به اسمِ const
که دو آرگومان میگیره و همیشه آرگومان اولِش رو برمیگردونه استفاده میکنیم. وقتی const
رو روی یه لیست فولد میکنیم، دو آرگومانش مقادیرِ لیست و مقدارِ acc
میشن – اینکه کدوم یکی آرگومانِ اولِش باشه بستگی به نوعِ فولدی داره که استفاده میکنیم. با مثال زیر شروع میکنیم:
Prelude> foldr const 0 [1..5]
(const 1 _)
1
از اونجا که const
آرگومانِ دومش رو اجبار نمیکنه، مابقیِ
فولد هم اصلاً محاسبه نشد. خط تیره مابقیِ فولد (که حساب نشد) رو نشون میده. حالا ببینیم اگه آرگومانها رو جابجا کنیم چی میشه:
Prelude> foldr (flip const) 0 [1..5]
0
جواب صفر شد چون مقدارِ acc
صفر بود.
حالا از همون تابع با foldl
مثال میزنیم. یه کم فکر کنین تا متوجهِ مراحلِ محاسبه تا رسیدن به این جوابها بشین:
Prelude> foldl (flip const) 0 [1..5]
5
Prelude> foldl const 0 [1..5]
0
این تأثیرِ شرکتپذیری از چپ ِه. پیمایش ِ ستون برای فولد ِ چپ و راست یکسانه – به خاطر نحوهی تعریف شدنِ لیستها، باید یکسان باشن. بسته به تابع فولدینگ، فولد ِ چپ ممکنه جواب متفاوتی از فولد ِ راست بده.
تمرینها: درک فولدها
۱.
جواب کدوم یکی از بیانیههای زیر با جوابِ foldr (*) 1 [1..5]
برابره؟
a)
flip (*) 1 [1..5]
b)
foldl (flip (*)) 1 [1..5]
c)
foldl (*) 1 [1..5]
۲.
مراحل محاسبه برای بیانیهی زیر رو بنویسید:
oldl (flip (*)) 1 [1..3]
۳.
یه فرق بینِ foldr
و foldl
اینه که:
a)
foldr
، برخلافِ foldl
، ستون ِ یه لیست رو از راست به چپ پیمایش میکنه.
b)
تابعِ foldr
، برخلافِ foldl
، همیشه مابقیِ فولد رو اجبار میکنه.
c)
تابعِ foldr
، برخلاف foldl
، به راست شرکت میده.
d)
تابعِ foldr
، برخلافِ foldl
، بازگشتی ِه.
۴.
فولدها کاتامورفیسم اند، یعنی عموماً برای کدوم یکی از کارهای زیر استفاده میشن؟
a)
کاهشِ ساختار
b)
گسترشِ ساختار
c)
شیوع روانگسیختگی کاتاتونی
d)
تولید ساختارهای دادهی بینهایت
۵.
گزینههای زیر، فولدهایی مشابهِ چیزهایی که تا الان دیدین هستن، ولی هر کدوم حداقل یک خطا دارن. بعد از تصحیحشون در REPL تست کنین:
a)
foldr (++) ["woot", "WOOT", "woot"]
b)
foldr max [] "fear is the little death"
c)
foldr and True [False, True]
d)
این یکی یه ذره زیرکانهتر از قبلیه. اصلاً ممکن هست جوابِ متفاوتی برگردونه؟
foldr (||) True [False, True]
e)
foldl ((++) . show) "" [1..5]
f)
foldr const 'a' [1..5]
g)
foldr const 0 "tacos"
h)
foldl (flip const) 0 "burritos"
i)
foldl (flip const) 'z' [1..5]
پیشروی ِ بیقیدوشرط از روی ستون
در فولد از چپ، مراحل بعدیِ فولد به عنوان آرگومانِ اولِ تابع فولدینگ داده میشه، و این یکی از تفاوتهای مهم بینِ foldr
و foldl
ِه. برخلافِ foldr
، در foldl
تابعِ فولدینگ هیچ مداخلهای در پیشروی روی ستون نمیکنه، به عبارت دیگه، پیشرویش روی ستون بیقیدوشرط ِه. به کار بردنِ تابعی که محاسبهی هیچ کدوم از آرگومانهاش رو اجبار نمیکنه تأثیری نداره. دوباره const
رو نگاه کنیم:
Prelude> const 1 undefined
1
Prelude> (flip const) 1 undefined
*** Exception: Prelude.undefined
Prelude> (flip const) undefined 1
1
حالا با این مقایسه کنین:
Prelude> let xs = [1..5] ++ undefined
Prelude> foldr const 0 xs
1
Prelude> foldr (flip const) 0 xs
*** Exception: Prelude.undefined
Prelude> foldl const 0 xs
*** Exception: Prelude.undefined
Prelude> foldl (flip const) 0 xs
*** Exception: Prelude.undefined
با اینکه foldl
بیقیدوشرط ستون رو محاسبه میکنه، اما هنوز میشه روی محاسبهی مقادیرِ داخلِ لیست اختیار داشت. مثالِ زیر به خاطر تهی بودنِ بخشی از ستون خطا میده چون foldl
تمامِ ستون رو محاسبه میکنه:
Prelude> let xs = [1..5] ++ undefined
Prelude> foldl (\_ _ -> 5) 0 xs
*** Exception: Prelude.undefined
اما در این مثال، تهی یکی از مقادیره و موردی نداره:
Prelude> let xs = [1..5] ++ [undefined]
Prelude> foldl (\_ _ -> 5) 0 xs
5
این خصوصیت نشون میده که عموماً foldl
برای لیستهایی که بینهایت هستن (یا ممکنه باشن)، مناسب نیست. از طرف دیگه، این محاسبهی بیقیدوشرط ِ ستون، و در کنارش نااکید بودن، یعنی استفاده از foldl
برای لیستهای طولانی هم معمولاً خوب نیست، چون اجبار برای محاسبهی ستون، عملکرد رو هم تضعیف میکنه. foldl
قبل از اینکه شروع به محاسبهی هر سلول کنه، باید کلِ ستون رو محاسبه کنه. همین میشه که با پیمایش از روی ستون، تعداد زیادی مقادیرِ محاسبهنشده رو تلنبار میکنه.
در اکثر موارد که به فولد از چپ احتیاج دارین، باید از foldl'
استفاده کنین. این تابع که "فولد-اِل-پرایم" خونده میشه، مثلِ foldl
کار میکنه، با این تفاوت که اکید ِه. یعنی با پیمایش ِ ستون، بجای تلنبار کردنِ بیانیههای محاسبه نشده به ازای هر المانِ لیست، مقادیرِ داخل سلولهای cons رو هم اجبار میکنه. این محاسبهی اکید، تأثیر منفیِ فولد کردن ِ لیستهای طولانی از چپ رو کم میکنه.