۱۶ - ۶خوب، بد، زشت
با چندتا مثال، مفهومِ قانونمند بودن یا قانونشکن بودن رو برای نمونههای 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 نیست رو به چشمِ جزئی از ساختاری که توابع باید از روشون لیفت بشن نگاه کنین.