۱۹ - ۳انتقام مانویدها

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