۱۹ - ۳انتقام مانویدها
یکی از مواردی که موقعِ فولدها بهش اشاره نکردیم، اهمیتِ مانویدها بود. فولدینگ لزوماً یک عملیاتِ باینری و شرکتپذیر، به همراهِ یک مقدار همانی رو القا میکنه. دو عملیاتی که در 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
دیدیم اینه که یه تعمیمی از کاتامورفیسمها – فولدینگ – برای نوعدادههای مختلف ِه، و در بعضی موارد، شما رو مجبور میکنه راجع به مانویدی که برای ترکیب ِ مقادیر استفاده میکنین فکر کنین.