۱۹ - ۳انتقام مانویدها
یکی از مواردی که موقعِ فولدها بهش اشاره نکردیم، اهمیتِ مانویدها بود. فولدینگ لزوماً یک عملیاتِ باینری و شرکتپذیر، به همراهِ یک مقدار همانی رو القا میکنه. دو عملیاتی که در Foldable تعریف شدن چنین الزامی رو صراحتاً بیان میکنن:
class Foldable t (t :: * -> *) where
fold :: Monoid m => t m -> m
foldMap :: Monoid m
=> (a -> m) -> t a -> mبا fold میشه توسطِ Monoid ای که برای المانهای داخلِ یه ساختار ِ Foldable تعریف شده، اون المانها رو با هم ترکیب کرد. کاری که foldMap انجام میده اینه که اول تکتکِ المانهای داخلِ ساختار ِ Foldable رو به یه Monoid نگاشت میده، و بعد نتیجهها رو با نمونه ِ Monoid ِشون ترکیب میکنه.
اگه به اون الزامِ Foldable برای Monoid دقت نکنین، ممکنه اینها یه کم عجیب به نظر برسن. یه عملیات خیلی پایهای با foldr ببینیم و با fold و foldMap مقایسه کنیم.
Prelude> foldr (+) [1..5]
15عملیات ِ باینری و شرکتپذیر در این فولد، تابعِ (+) ِه، پس در واقع مانوید رو به صورتِ ضمنی تعیین کردیم. اینکه اعداد بیشتر از یک مانوید دارن مهم نیست، چون تعیین کردیم از چه عملیاتی استفاده شه.
همینطوری از تایپِ fold مشخص ِه همون کارِ foldr رو انجام نمیده، چون آرگومانِ اولش تابع نیست. غیر از اون هم، نمیشه یه لیستِ اعداد رو همینطوری فولد کرد؛ تابع fold هیچ Monoid ای تعیین نکرده:
Prelude> fold (+) [1, 2, 3, 4, 5]
-- پیغام خطا ناشی از تعداد آرگومانها
Prelude> fold [1, 2, 3, 4, 5]
-- پیغام خطا به خاطر نداشتن
-- Monoid نمونهیپس برای اینکه fold کار کنه، باید یه نمونه ِ Monoid مشخص کنیم:
Prelude> let xs = map Sum [1..5]
Prelude> fold xs
Sum {getSum = 15}یا یه کم خوشایندتر:
Prelude> :{
*Main| let xs :: Sum Integer
*Main| xs = [1, 2, 3, 4, 5]
*Main| :}
Prelude> fold xs
Sum {getSum = 15}
Prelude> :{
*Main| let xs :: Product Integer
*Main| xs = [1, 2, 3, 4, 5]
*Main| :}
Prelude> fold xs
Product {getProduct = 120}در بعضی موارد، خودِ کامپایلر تشخیص میده و از Monoid ِ استانداردِ یه تایپ، بدونِ اینکه لازم باشه صراحتاً بگیم استفاده میکنه:
Prelude> foldr (++) "" ["hello", " julie"]
"hello julie"
Prelude> fold ["hello", " julie"]
"hello julie"خودش از Monoid ِ پیشفرض برای لیست استفاده کرد، و لازم هم نبود چیزی بگیم.
حالا نوبت یه چیز متفاوت
بریم سراغِ foldMap. برخلافِ fold، اولین آرگومانِ foldMap یه تابعه. و برخلافِ foldr، اولین آرگومان (تابعی) foldMap باید صراحتاً هر المانِ ساختار رو به یه Monoid نگاشت کنه:
Prelude> foldMap Sum [1, 2, 3, 4]
Sum {getSum = 10}
Prelude> foldMap Product [1, 2, 3, 4]
Product {getProduct = 24}
Prelude> foldMap All [True, False, True]
All {getAll = False}
Prelude> foldMap Any [(3 == 4), (9 > 5)]
Any {getAny = True}
Prelude> let xs = [Just 1, Nothing, Just 5]
Prelude> foldMap First xs
First {getFirst = Just 1}
Prelude> foldMap Last xs
Last {getLast = Just 5}در مثالهای بالا، تابعهایی که اعمال کردیم در واقع دادهساز بودن، و نمونه ِ Monoid (یعنی mappend) رو برای اون تایپها تعیین میکنن. با همینها، foldMap اطلاعات کافی برای کاهش ِ مجموعهی مقادیر به یه مقدار خلاصه رو داره.
تابعی که foldMap نگاشت میکنه، ممکنه متفاوت از Monoid ای که استفاده میکنه باشه:
Prelude> let xs = map Product [1..3]
Prelude> foldMap (*5) xs
Product {getProduct = 750}
-- 5 * 10 * 15 == 750
Prelude> let xs = map Sum [1..3]
Prelude> foldMap (*5) xs
Sum {getSum = 30}
-- 5 + 10 + 15 == 30اول تابع رو به تکتکِ مقادیر نگاشت میده و بعد با استفاده از نمونه ِ Monoid، اونها رو به یک مقدار کاهش میده. این رو با foldr مقایسه کنین که تابعِ فولدینگِش نمونه ِ Monoid رو در دلِ خودش داره:
Prelude> foldr (*) 5 [1, 2, 3]
-- (1 * (2 * (3 * 5)))
30در حقیقت به خاطرِ طرزِ کارِ foldr، تعیینکردنِ یه نمونه ِ Monoid متفاوت از اونی که از تابع فولدینگ القا میشه، تأثیری در نتیجهی نهایی نداره:
Prelude> let sumXs = map Sum [2..4]
Prelude> foldr (*) 3 sumXs
Sum {getSum = 72}
Prelude> let productXs = map Product [2..4]
Prelude> foldr (*) 3 productXs
Product {getProduct = 72}البته خوبه که اشاره کنیم، اگه چیزی که میخواین فولد کنین فقط حاویِ یک مقدار باشه، تعیین یه نمونه ِ Monoid رفتارِ foldMap رو تغییری نمیده:
Prelude> let fm = foldMap (*5)
Prelude> fm (Just 100) :: Product Integer
Product {getProduct = 500}
Prelude> fm (Just 5) :: Sum Integer
Sum {getSum = 25}فقط با یک مقدار، نیازی به نمونه ِ Monoid نداره. نمونه ِ Monoid رو باید تعیین کرد تا تایپچکر راضی بشه، اما وقتی فقط یک مقدار هست، چیزی برای mappend وجود نداره. فقط تابع رو اعمال میکنه. اگه مقداری که میخواین فولد کنین خالی باشه، اون موقع از mempty ِ تعریفشده در نمونه ِ Monoid استفاده میکنه:
Prelude> fm Nothing :: Sum Integer
Sum {getSum = 0}
Prelude> fm Nothing :: Product Integer
Product {getProduct = 1}پس چیزی که تا اینجا از Foldable دیدیم اینه که یه تعمیمی از کاتامورفیسمها – فولدینگ – برای نوعدادههای مختلف ِه، و در بعضی موارد، شما رو مجبور میکنه راجع به مانویدی که برای ترکیب ِ مقادیر استفاده میکنین فکر کنین.