۱۶ - ۶خوب، بد، زشت
با چندتا مثال، مفهومِ قانونمند بودن یا قانونشکن بودن رو برای نمونههای Functor
بهتر میشه نشون داد. با تعریفِ یه نوعساز ِ تکآرگومانی شروع میکنیم:
data WhoCares a =
ItDoesnt
| Matter a
| WhatThisIsCalled
deriving (Eq, Show)
این نوعداده فقط یک دادهسازی داره که شاملِ یه مقداره و میشه از روش fmap
کرد، اون هم Matter
ِه. بقیهشون دادهسازهای پوچگانه اند، پس هیچ مقداری داخلشون نیست که باهاش کاری انجام بدیم. فقط یک ساختار وجود داره.
یه مثال از یک نمونه ِ قانونمند:
instance Functor WhoCares where
fmap _ ItDoesnt = ItDoesnt
fmap _ WhatThisIsCalled =
WhatThisIsCalled
fmap f (Matter a) = Matter (f a)
اگه نمونهای که تعریف کردیم از قانون همانی تبعیت نکنه، فانکتور ِ معتبری نیست. اون قانون میگه که fmap id (Matter _)
اصلاً نباید به Matter
دست بزنه – به عبارتِ دیگه باید با id (Matter _)
یکی باشه. Functor
راهی برای لیفت کردن از روی ساختار (نگاشت کردن) بدونِ اهمیت به اون ساختار ِه، کلاً اجازهی دستکاریِ ساختار رو نداره.
حالا یه نمونه ِ قانونشکن رو ببینیم:
instance Functor WhoCares where
fmap _ ItDoesnt = WhatThisIsCalled
fmap f WhatThisIsCalled = ItDoesnt
fmap f (Matter a) = Matter (f a)
اینجا میشه بیشتر روی مفهوم دستکاری نکردنِ ساختار صحبت کرد. کاری که با ItDoesnt
و WhatThisIsCalled
تو این نمونه انجام دادیم، در واقع خودِ ساختار (نه مقادیری که داخلِ ساختار قرار گرفتن) رو تغییر داده. خیلی سریع میشه دید که چرا خرابه:
Prelude> fmap id ItDoesnt
WhatThisIsCalled
Prelude> fmap id WhatThisIsCalled
ItDoesnt
Prelude> fmap id ItDoesnt == id ItDoesnt
False
Prelude> :{
*Main| fmap id WhatThisIsCalled ==
*Main| id WhatThisIsCalled
*Main| :}
False
واضحه که از قانون همانی تبعیت نمیکنه. نمونه ِ معتبری از فانکتور نیست.
حالا اگه بخواین که تابع علاوه بر مقدار، ساختار رو هم تغییر بده چطور؟
خبر خوب اینکه چنین چیزی وجود داره! همون تابعِ معمولی که از قدیم داشتیمه. یکی بنویسین. هرچندتا که میخواین بنویسین! هدفِ Functor
ارائهی یه حالتِ تخصصیتر برای مواقعیه که ساختار ِ اضافه وجود داره و میخوایم از همون توابعی که داشتیم دوباره استفاده کنیم. بالاتر دیدیم که Functor
در واقع یه حالتِ خاص از اعمال ِ تابعست؛ همین خاص بودن یعنی ترجیح میدیم قابلیتهایی که اون رو متمایز میکنه حفظ کنیم. پس قانونها رو رعایت میکنیم.
جلوتر تو همین فصل، یه چیزی که میشه "معکوسِ فانکتور" دونست معرفی میکنیم – ساختار رو تغییر میده و مقدار رو دست نمیزنه. اون هم یه اسم مخصوص داره، اما تایپکلاسی که همه تأییدش کرده باشن نداره.
ترکیب هم باید کار کنه
خیلی خوب، حالا که دیدیم چطور یه نمونه ِ Functor
میتونه قانون همانی رو نقض کنه، ببینیم چطور میشه از قانون ترکیبپذیری تبعیت کرد (و نقضش کرد!). شاید از بالاتر این قانون یادتون باشه:
fmap (f . g) == fmap f . fmap g
این قانون میگه ترکیب ِ دو تابعی که به طورِ مجزا لیفت شدن، باید همون جوابی رو بده که اگه اون دو تابع رو اول با هم ترکیب میکردیم و بعد تابعِ ترکیبشده رو لیفت میکردیم. حفظ این مشخصه به ترکیبپذیری ِ کُد کمک میکنه، و از اتفاقاتِ عجیب و ناخوشایند جلوگیری میکنه. دوباره یه نمونه ِ نامعتبر ِ Functor
مثال میزنیم:
data CountingBad a =
Heisenberg Int a
deriving (Eq, Show)
-- نیست OK اصلاً
instance Functor CountingBad where
fmap f (Heisenberg n a) =
-- (a -> b) f a =
Heisenberg (n+1) (f a)
-- f b
چی کار کردیم؟ CountingBad
یه آرگومانِ تایپی داره، اما Heisenberg
دو آرگومان داره. اگه با تایپِ fmap
مقایسه کنین شاید ببینین مشکل کجاست. اون n
که آرگومانِ Int
از Heisenberg
رو نشون میده، معادلِ کدوم بخش از تایپِ fmap
میشه؟
میشه این چِندِش رو تو REPL امتحان کنیم ببینیم ترکیب ِ دوتا fmap
یک جواب رو نمیدن، و در نتیجه قانون ترکیبپذیری رعایت نمیشه:
Prelude> let u = "Uncle"
Prelude> let oneWhoKnocks = Heisenberg 0 u
Prelude> fmap (++" Jesse") oneWhoKnocks
Heisenberg 1 "Uncle Jesse"
Prelude> let f = ((++"Jesse").(++" lol"))
Prelude> fmap f oneWhoKnocks
Heisenberg 1 "Uncle lol Jesse"
تا اینجا به نظر خوب میاد، اما اگه دوتا تابعِ الحاق رو جداگانه ترکیب کنیم چطور؟
Prelude> let j = (++ " Jesse")
Prelude> let l = (++ " lol")
Prelude> fmap j . fmap l $ oneWhoKnocks
Heisenberg 2 "Uncle lol Jesse"
یا بخوایم بیشتر شبیه خودِ قانون بنویسیم:
Prelude> let f = (++ " Jesse")
Prelude> let g = (++ " lol")
Prelude> fmap (f . g) oneWhoKnocks
Heisenberg 1 "Uncle lol Jesse"
Prelude> fmap f . fmap g $ oneWhoKnocks
Heisenberg 2 "Uncle lol Jesse"
واضحه که
fmap (f . g) == fmap f . fmap g
برقرار نیست. خوب چطور میشه درستش کرد؟
data CountingGood a =
Heisenberg Int a
deriving (Eq, Show)
-- همه چیز خوب
instance Functor CountingGood where
fmap f (Heisenberg n a) =
Heisenberg (n) (f a)
اگه با Int
وَر نریم اوکِی میشه. هرچیزی که آخرین آرگومانِ تایپیِ f
نیست رو به چشمِ جزئی از ساختاری که توابع باید از روشون لیفت بشن نگاه کنین.