۱۶ - ۶خوب، بد، زشت

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