۱۷ - ۴فانکتورهای اپلیکتیو، فانکتورهای مانویدیاند
اول به یه چیزی دقت کنیم:
($) :: (a -> b) -> a -> b
(<$>) :: (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f b
از قبل میدونیم که $
در واقع یه تابعِ میانوند ِ هیچکارهست که فقط نقشِ افزایشِ تقدم ِ سمتِ راست و کاهشِ پرانتزها رو داره. ولی اینجا کمک میکنه مفهومی رو نشون بدیم.
وقتی با <$>
(مستعار ِ fmap
) مقایسهش میکنیم، اولین تغییری که متوجه میشیم اینه که (a -> b)
رو از روی f
که مقدارِ a
رو پوشونده لیفت، و بعد بهش اعمال میکنیم.
به اَپ یا <*>
(متود ِ اعمال ِ Applicative
) که میرسیم، میبینیم که تابعمون هم داخلِ اون ساختار ِ فانکتوری پوشونده شده. حالا میرسیم به مانویدی در "فانکتور مانویدی":
:: f (a -> b) -> f a -> f b
-- دو آرگومان تابع اینها هستن:
f (a -> b)
-- و
f a
اگه فرض کنیم با نادیده گرفتن اون ساختار ِ فانکتوری، (a -> b)
رو به a
اعمال کنیم تا b
بگیریم، باز هم یه مشکل هست، چون خروجیمون باید f b
باشه. وقتی با fmap
کار میکردیم، فقط با یک ساختار سروکار داشتیم، پس دستنخورده رهاش میکردیم. ولی حالا دوتا ساختار با تایپِ f
داریم که برای رسیدن به f b
باید یه کاری باهاشون بکنیم. نمیشه کاری به کارشون نداشته باشیم؛ باید یه جوری با هم متحدشون کنیم. میدونیم که f
در طولِ تایپ تغییری نمیکنه، پس قطعاً ساختارِ هردوشون از یه تایپه. اگه بخشهای ساختاری رو از بخشهای تابعی جدا کنیم، شاید ببینیم چه چیزی لازم داریم:
:: f (a -> b) -> f a -> f b
f f f
(a -> b) a b
قبلاً یه چیزی نداشتیم که دوتا مقدار از یه تایپ میگرفت و یه مقدار از همون تایپ برمیگردوند؟ با فرضِ اینکه f
تایپی با یه نمونه ِ Monoid
باشه، راهِ خوبی برای ترکیبشون داریم:
mappend :: Monoid a => a -> a -> a
پس با Applicative
، یه مانوید برای ساختارِمون داریم و یه اعمالِ تابع برای مقادیرمون!
mappend :: f f f
$ :: (a -> b) a b
(<*>) :: f (a -> b) -> f a -> f b
-- Functor از fmap به علاوهی
-- .نگاشت کرد f تا بشه از روی
پس میشه گفت یه Monoid
رو به یه Functor
میخ کردیم تا از تابعهایی که داخل ساختارهای اضافه هستن استفاده کنیم. به عبارتِ دیگه، داریم همون ساختاری که قبلاً با Functor
از روش نگاشت میکردیم رو به اعمالِ تابع اضافه میکنیم. با چندتا مثالِ آشنا بیشتر توضیح میدیم:
-- List
[(*2), (*3)] <*> [4, 5]
=
[2 * 4, 2 * 5, 3 * 4, 3 * 5]
-- سادهشده
[8,10,12,15]
پس در f (a -> b) -> f a -> f b
چه چیزی به (a -> b)
اضافه شده بود؟ در این مورد، "لیست بودن." با اینکه اعمال ِ هرکدوم از (a -> b)
ها به مقادیرِ تایپِ a
خیلی معمولیه، اینجا برخلافِ Functor
ِ لیست که یک تابع میداشتیم، یه لیست از توابع داریم.
اما لیستها تنها ساختارهایی که میشه به مقادیرمون اضافه کنیم نیستن – اصلاً اینطور نیست! میشه Maybe
هم باشه:
Just (*2) <*> Just 2
=
Just 4
Just (*2) <*> Nothing
=
Nothing
Nothing <*> Just 2
=
Nothing
Nothing <*> Nothing
=
Nothing
در Maybe
، فانکتور تابع رو با احتمالِ وجود نداشتنِ یه مقدار اعمال میکنه. با Applicative
، اون تابع هم ممکنه وجود نداشته باشه. چندتا مثالِ خوشگل و طولانی از چنین حالتی میبینیم – که چطور ممکنه به نقطهای برسیم که هیچ تابعی برای اعمال نداشته باشیم – یه ذره جلوتر، و نه فقط با Maybe
، با Either
و یه تایپِ جدید به اسمِ Validation
هم میبینیم.
مانوید رو نشونم بده
اگه خاطرتون باشه، نمونه ِ Functor
برای توپل ِ دوتایی، مقدارِ اول در توپل رو نادیده میگیره:
Prelude> fmap (+1) ("blah", 0)
("blah",1)
اما Applicative
ِ توپل ِ دوتایی، نقشِ مانوید در Applicative
رو به خوبی نشون میده. با دستور :info
برای (,)
در REPL، میشه این رو دید:
Prelude> :info (,)
data (,) a b = (,) a b
-- Defined in ‘GHC.Tuple’
...
instance Monoid a
=> Applicative ((,) a)
-- Defined in ‘GHC.Base’
...
instance (Monoid a, Monoid b)
=> Monoid (a, b)
در نمونه ِ Applicative
برای توپل ِ دوتایی، نیازی به یه Monoid
برای b
نداریم چون اون رو از طریقِ اعمال تابع درست میکنیم. ولی برای مقدارِ اولِ Monoid
لازم داریم، چون دوتا ازش داریم و باید به نحوی اون دوتا رو یکی کنیم:
Prelude> ("Woo", (+1)) <*> (" Hoo!", 0)
("Woo Hoo!",1)
دقت کنین که برای مقدارِ a
هیچ تابعی اعمال نکردیم، ولی انگار با جادو خودشون با هم ترکیب شدن؛ این به خاطرِ نمونه ِ Monoid
برای مقادیرِ a
ِه. تابعی که در جایگاهِ b
ِ توپل ِ سمتِ چپ قرار گرفته، به مقداری که در جایگاهِ b
ِ توپل ِ سمتِ راست قرار گرفته اعمال شده تا یه جواب درست کنه. همین اعمالِ تابع، دلیلِ بینیاز بودنِ b
به یه نمونه ِ Monoid
ِه.
چندتا مثالِ دیگه هم ببینیم. به نحوهی ترکیب ِ مقادیرِ a
خوب توجه کنین:
Prelude> import Data.Monoid
Prelude> (Sum 2, (+1)) <*> (Sum 0, 0)
(Sum {getSum = 2},1)
Prelude> (Product 3, (+9)) <*> (Product 2, 8)
(Product {getProduct 6 = False}, 17)
Prelude> (All True, (+1)) <*> (All False, 0)
(All {getAll = False},1)
چه Monoid
ای باشه مهم نیست، فقط باید بشه با هم ترکیب بشن، یا بینشون یکی انتخاب بشه.
مانوید و اپلیکتیو ِ توپل کنار هم
اگه نمیبینین، پلکهاتون رو به هم نزدیک کنین.
instance (Monoid a, Monoid b)
=> Monoid (a,b) where
mempty = (mempty, mempty)
(a, b) `mappend` (a',b') =
(a `mappend` a', b `mappend` b')
instance Monoid a
=> Applicative ((,) a) where
pure x = (mempty, x)
(u, f) <*> (v, x) =
(u `mappend` v, f x)
مانوید و اپلیکتیو ِ Maybe
با اینکه اپلیکتیو ها، فانکتور های مانویدی هستن، حواستون باشه که برمبنای این، چیزی رو فرض نکنین. یه دلیلش اینه که نه تضمینی هست، و نه لازمه که نمونههای Monoid
و Applicative
از مانوید ِ یکسان برای ساختارِشون استفاده کنن، بخشِ فانکتوری هم ممکنه رفتارش رو تغییر بده. با همهی اینها میشه Monoid
رو در نحوهی تطبیق الگو ِ Applicative
روی Just
و Nothing
دید و با این Monoid
مقایسه کرد:
instance Monoid a => Monoid (Maybe a) where
mempty = Nothing
mappend m Nothing = m
mappend Nothing m = m
mappend (Just a) (Just a') =
Just (mappend a a')
instance Applicative Maybe where
pure = Just
Nothing <*> _ = Nothing
_ <*> Nothing = Nothing
Just f <*> Just a = Just (f a)
بعداً چندتا مثال میبینیم که چطور نمونههای Monoid
ِ مختلف ممکنه نتیجههای مختلفی از اپلیکتیو بدن. فعلاً متوجه باشین که تیکهی مانویدی ِ اپلیکتیو ممکنه اون چیزی که شما به عنوانِ mappend
ِ استاندارد یا کانونیک فرض میکنین نباشه؛ چون بعضی تایپها ممکنه چندتا مانوید داشته باشن.