۱۰ - ۴فولد از راست
به foldr
فولد از راست میگیم چون فولد شرکتپذیری از راست داره؛ یا به کلام دیگه، از راست پرانتزگذاری میشه. این رو در تعریفِ foldr
هم میشه دید:
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f z [] = z
foldr f z (x:xs) = f x (foldr f z xs)
حتماً شباهتهای بین این و بقیهی تابعهای بازگشتی که بالاتر دیدیم براتون واضحه. یکی از آرگومانهای تابعِ f
(تابعِ فولدینگ)، "مابقیِ فولد" (یا foldr f z xs
) هست. z
میشه صفرِ فولد؛ هم یه مقدار پشتیبان برای وقتی که لیستِ ورودی به foldr
خالی باشه، هم یه آرگومانِ دومی که اجازه میده فولد رو شروع کنیم. معمولاً از مقدار همانی ِ تابعِ فولدینگ برای این صفر استفاده میشه، مثل عدد صفر برای (+)
، و ۱ برای (*)
.
foldr
چطور محاسبه میکنه
برای اینکه بتونیم راحتتر توضیح بدیم، یه کم تعریفی که بالاتر از foldr
نوشتیم رو تغییر میدیم. البته این تغییرات تأثیری روی مفهومِ تعریف نمیذارن:
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f z xs =
case xs of
[] -> z
(x:xs) -> f x (foldr f z xs)
اینجا میشه دید که فولد از راست، شرکتپذیری از راست داره. این هم مشابهِ مثالِ sum
که بالاتر نوشتیم ساده میشه:
foldr (+) 0 [1, 2, 3]
اولین قدم برای ساده کردنِ این فولد، جایگذاریِ آرگومانِ ورودی بجای xs
در بیانیهی case ِه:
foldr (+) 0 [1, 2, 3]
case [1, 2, 3] of
...
با کدوم یکی از حالتها انطباق داره؟
foldr (+) 0 [1, 2, 3]
case [1, 2, 3] of
[] -> z
(x:xs) -> f x (foldr f z xs) -- <-- این یکی
خب، در اون شاخهی case، متغیرهای f
، x
، xs
، و z
به چه مقادیری مقیّد شدن؟
foldr (+) 0 [1, 2, 3]
case [1, 2, 3] of
[] -> 0
(1 : [2, 3]) ->
(+) 1 (foldr (+) 0 [2, 3])
از اونجا که (+)
در هر دو آرگومانش اَکید ِه، ما هم (foldr (+) 0 [2,3])
رو باز میکنیم؛ یعنی مرحلهی بعدیِ این محاسبهی بازگشتی رو اجبار میکنیم. البته ممکنه تابع فولدینگ تابعی باشه که مابقیِ فولد رو مکرراً اجبار نکنه. برای مثال const
چنین تابعیه؛ همیشه اولین آرگومانش رو برمیگردونه (یه کم جلوتر بیشتر توضیح میدیم). پس اینجا، خوداتکایی ِ بعدی میشه (foldr (+) 0 [2,3])
:
foldr (+) 0 [2, 3]
case [2, 3] of
[] -> 0 -- باز هم این منطبق نشد
(2 : [3]) -> (+) 2 (foldr (+) 0 [3])
به طور ضمنی، تابعِ (+) 1
از بیرون به کل کُدِ بالا اعمال شده. تابعِ (+)
به هر دو آرگومانش اکید بودن ِ بیقیدوشرط داره، پس باز هم خوداتکایی ِ تابعِ foldr
رو ادامه میدیم. میبینید که یکیدَرمیون داریم تابعِ f
و foldr
رو صدا میزنیم. این رفت و برگشت، بیشترِ اختیار رو به تابعِ فولدینگ (تابعی که باهاش فولد میکنیم) میده. یه تابع فولدینگ مثل const
، با محاسبه نکردنِ آرگومان دومش (که همون "مابقیِ فولد" ِه)، تعیین کنندهی کاهش میزان کارِ محاسباتی میشه.
مشابهِ بالا، تابعِ (+) 1 ((+) 2 ...)
به صورت ضمنی دورِ کُد زیر رو پوشونده:
foldr (+) 0 [3]
case [3] of
[] -> 0 -- باز هم این منطبق نشد
(3 : []) -> (+) 3 (foldr (+) 0 [])
یک بار دیگه، برای بار آخر، foldr
رو صدا میزنیم. باز هم دورِ کُد زیر به صورت ضمنی با (+) 1 ((+) 2 ((+) 3...))
پوشونده شده. حالا میرسیم به فرمان پایه:
foldr (+) 0 []
case [] of
[] -> 0 -- بالاخره این یکی منطبق شد
-- اون یکی حالت دیگه مهم نیست
در نتیجه، میشه به روشِ محاسبهی هسکل به چشم یه سیستم بازنویسیِ نوشته نگاه کرد. بیانیهی ما تا اینجا، خودش رو از این فرم:
foldr (+) 0 [1, 2, 3]
به این فرم بازنویسی کرده:
(+) 1 ((+) 2 ((+) 3 0))
اگه بخوایم تمیزتر بنویسیم، بدون اینکه شیوهی محاسبهش رو تغییر بدیم، اینجوری میشه:
1 + (2 + (3 + 0))
مثل حساب، داخلیترین پرانتز رو اول حساب میکنیم:
1 + (2 + (3 + 0))
1 + (2 + 3)
1 + 5
6
حالا محاسبه تموم شد، با جواب ۶.
برای دیدنِ نحوهی شرکتپذیری در فولد، میتونیم از یه کَلَکی که توسطِ چندتا از کاربران در جامعهی IRC ِ هسکل معروف شد هم استفاده کنیم.*
xs = map show [1..5]
y = foldr (\x y -> concat
["(",x,"+",y,")"]) "0" xs
ایده متعلق به کیل گیبارد از کانالِ IRC ِ هسکل Freenode، و همچنین در صفحهی wiki ِ هسکل.
با صدا زدنِ y
در REPL، نحوهی محاسبهی foldr
رو میبینیم:
Prelude> y
"(1+(2+(3+(4+(5+0)))))"
یکی از جنبههای فولد کردن که ممکنه از اول مشخص نباشه، اینه که در دو مرحله اتفاق میوفته: پیمایش و فولدینگ. به مرحلهای که فولد از روی ستون پیش میره، مرحلهی پیمایش میگیم. منظور از فولدینگ هم محاسبه یا سادهسازی نتیجهی حاصل از اعمال ِ تابعِ فولدینگ به مقادیره. همهی فولدها در یک جهت از روی ستون رد میشن؛ تفاوت بین فولد از چپ با فولد از راست، در شرکت دادن (یا پرانتزگذاری) ِ تابعِ فولدینگ ِشونِه*، که در نتیجه جهتِ فولد شدن یا محاسبه رو تعیین میکنه.
م. منظور جهتِ شرکتپذیری ِ خودِ تابع نیست، بلکه جهت اعمال شدن ِ تابعست.
در foldr
، "مابقیِ فولد" میشه آرگومان دومِ تابعی که باهاش فولد میکنیم:
foldr f z (x:xs) = f x (foldr f z xs)
-- ^------------^
-- مابقی فولد
به خاطرِ این محاسبهی دو مرحلهای و نااکید، اگه f
آرگومانِ دومش (مابقی فولد) رو محاسبه نکنه، بقیهی ستون هم اجبار نمیشه. در نتیجه foldr
علاوه بر قابلیت اجتناب از محاسبهی بعضی یا همهی مقادیرِ لیست، میتونه از محاسبهی بخشی یا کلِ ستون ِ لیست هم اجتناب کنه! به همین دلیل foldr
برای لیستهایی که به احتمال بینهایتاند هم قابل استفادهست. برای نمونه، مثالهای زیر رو مقایسه کنین (یادتون باشه که (+)
بیقیدوشرط، کلِ ستون و همهی مقادیر رو حساب میکنه):
Prelude> foldr (+) [1..5]
15
foldr
به همراه جمع، برای لیستِ بینهایت قابل استفاده نیست، اما اگه بجای جمع از توابعی که در هر دو آرگومانشون اکید نیستن استفاده کنیم، برای رسیدن به جواب نیاز به محاسبهی همهی مقادیر نداریم و در نتیجه foldr
هم قابل اعمال میشه. برای مثال، تابع myAny
به محض دیدنِ یک مقدارِ True
جوابِ True
برمیگردونه:
myAny :: (a -> Bool) -> [a] -> Bool
myAny f xs =
foldr (\x b -> f x || b) False xs
این مثال، با اینکه لیستش بینهایته کار میکنه:
Prelude> myAny even [1..]
True
اما این یکی محاسبهش تموم نمیشه، چون همیشه فرد ِه:
Prelude> myAny even (repeat 1)
به چنین محاسبهی بیپایانی تهی یا undefined
هم میگیم. با فولد ِ لیستهای بینهایت تضمینی برای رسیدن به جواب نیست، حتی اگه از foldr
استفاده کنین؛ معمولاً بستگی به داده ِ ورودی و تابع فولدینگ داره. چند مثال با تهی ِ سادهتر رو ببینیم:
Prelude> let u = undefined
-- میدیم undefined اینجا یه مقدارِ
Prelude> foldr (+) 0 [1, 2, 3, 4, u]
*** Exception: Prelude.undefined
Prelude> let xs = take 4 [1, 2, 3, 4, u]
Prelude> foldr (+) 0 xs
10
-- بخشی از ستونه undefined اینجا
Prelude> let xs = [1, 2, 3, 4] ++ u
Prelude> foldr (+) 0 xs
*** Exception: Preldue.undefined
Prelude> let xs = take 4 ([1,2,3,4] ++ u)
Prelude> foldr (+) 0 xs
10
وقتی فقط ۴ المان اول رو میگیریم، فرایندِ فولدینگ ِ بازگشتی رو بعد از ۴ المانِ اول متوقف میکنیم و در نتیجه تابعِ جمع به تهی نمیخوره، چه یکی از مقادیر باشه، چه بخشی از ستون ِ لیست.
تابعِ length
رفتارِ متفاوتی داره؛ مقادیر رو محاسبه نمیکنه، ولی ستون رو بیقیدوشرط حساب میکنه:
Prelude> length [1, 2, 3, 4, undefined]
5
Prelude> length ([1, 2, 3, 4] ++ undefined)
*** Exception: Prelude.undefined
با این حال اگه قبل از استفاده از length
اون بخشی از ستون که شاملِ تهی میشه رو بندازیم، بیانیهمون کار میکنه:
Prelude> let xs = [1, 2, 3, 4] ++ undefined
Prelude> length (take 4 xs)
4
تابعِ take
مثل خیلی از چیزهای دیگهای که تا حالا دیدین نااکید ِه، و اینجا فقط همون قدر از لیست که خواستین رو برمیگردونه. تفاوتِ این تابع اینه که وقتی به طولی که بهش دادین میرسه، متوقف میشه. این مثال رو در نظر بگیرین:
Prelude> let xs = [1, 2] ++ undefined
Prelude> length $ take 2 $ take 4 xs
2
مهم نیست که take 4
به تهی میخورد! به خاطرِ take 2
ِ قبلش اصلاً اجبار نشد.
حالا که طرز کار آرگومان دوم به تابعِ فولدینگ ِ foldr
رو دیدیم، نگاهی به آرگومانِ اولش میندازیم:
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f z [] = z
foldr f z (x:xs) = f x (foldr f z xs)
-- [1]
آرگومانِ اول ([1]
) از یه تطبیق الگو، که به طور پیشفرض اکید ِه بدست میاد – تابعِ f
فقط در صورتی به x
اعمال میشه که یه مقدارِ x
وجود داشته باشه و یه لیستِ خالی نباشه. این به این معنیه که تابعِ foldr
باید یه سلول cons ِ اولیه رو اجبار کنه تا بتونه فرق بین دو حالتِ []
و (x:xs)
رو تشخیص بده. در نتیجه سلول cons ِ اول نمیتونه تهی یا تعریفنشده باشه.
پس foldr
بخشِ اولِ ستون رو محاسبه میکنه. سعی میکنیم با چندتا مثالِ غیرمعمول این رو نشون بدیم. اینجا یه تابعِ بینام و نسبتاً مسخره تعریف کردیم که همهی آرگومانهاش رو نادیده میگیره و مقدارِ ۹۰۰۱ رو برمیگردونه. چنین تابعی هیچ کدوم از آرگومانهاش رو مجبور به محاسبه نمیکنه، یعنی موردی نداره اگه بخشی از ستون ِ لیست، یا یکی از مقادیرِ داخلِ لیست تهی باشه:
Prelude> foldr (\_ _ -> 9001) 0 [1..5]
9001
Prelude> let xs = [1, 2, 3, undefined]
Prelude> foldr (\_ _ -> 9001) 0 xs
9001
Prelude> let xs = [1, 2, 3] ++ undefined
Prelude> foldr (\_ _ -> 9001) 0 xs
9001
تا وقتی بخشِ اولِ ستون تهی نباشه، همه چیز خوبه:
Prelude> foldr (\_ _ -> 9001) 0 undefined
*** Exception: Prelude.undefined
Prelude> let xs = [1, undefined]
Prelude> foldr (\_ _ -> 9001) 0 xs
9001
Prelude> let xs = [undefined, undefined]
Prelude> foldr (\_ _ -> 9001) 0 xs
9001
دوتا مثالِ آخر کار کردن چون اولین سلول cons ِشون تهی نبود – اون مقادیرِ تعریفنشده داخل سلولهای cons اند، نه خودِ ستون. به کلام دیگه، سلولهای cons حاوی مقادیر تهی اند، ولی خودشون تهی نیستن. بعداً با اکید بودن و نااکید بودن بیشتر آزمایش میکنیم تا تأثیرشون رو روی نحوهی محاسبه در برنامههامون ببینیم.
تا وقتی تابعِ فولدینگ نتیجهی حاصل از فولد کردنِ مابقی لیست رو نخواد، بقیهی لیست هم پیمایش نمیشه. در مثالهای زیر، به دلیل اینکه const
آرگومانِ دومش ("مابقی لیست") رو دور میندازه، پیمایش ِ ستون هم اجبار نمیشه:
-- :یادآوری
-- const :: a -> b -> a
-- const x _ = x
Prelude> const 1 2
1
Prelude> const 2 1
2
Prelude> foldr const 0 [1..5]
1
Prelude> foldr const 0 [1, undefined]
1
Prelude> foldr const 0 ([1, 2] ++ undefined)
1
Preldue> foldr const 0 [undefined, 2]
*** Exception: Prelude.undefined
حالا که طریقهی محاسبهی foldr
رو دیدیم، قبل از ادامه به نوشتن و استفاده از فولدها، نگاهی به foldl
میندازیم.