۱۸ - ۵قوانین موند

تایپکلاس ‏‎Monad‎‏ هم مثل بقیه‌ی تایپکلاس‌ها قوانینی داره. این قوانین هم مثل بقیه‌ی قانون‌ها برای اطمینان از خراب نبودنِ کُد وجود دارن. اگه نمونه ِ ‏‎Monad‎‏‌ای که برای تایپ‌تون می‌نویسین از این قوانین تبعیت کنه، دیگه مونَد‌هاتون رفتارِ غیرمنتظره‌ای انجام نمیدن. برای نوشتنِ یه نمونه فقط کافیه عملیات ِ ‏‎>>=‎‏ رو تعریف کنین، ولی تا جای ممکن باید رفتار قابل پیش‌بینی داشته باشه.

قوانین همانی

-- همانی از راست
m >>= return     =  m

-- همانی از چپ
return x >>= f   =  f x

اساساً هردوی اینها میگن که ‏‎return‎‏ باید خنثی باشه و هیچ محاسباتی انجام نده. اونها رو با تایپِ ‏‎>>=‎‏ مقایسه می‌کنیم تا واضح‌تر ببینیم:

(>>=) :: Monad m
      => m a -> (a -> m b) -> m b
--       [1]       [2]        [3]

اول همانی از راست:

return :: a -> m a

    m    >>= return    =  m
-- [1]        [2]        [3]

اون ‏‎m‎‏ به ترتیب یه ‏‎m a‎‏ و یه ‏‎m b‎‏ رو نشون میده، پس با اینکه شاید از نحوه‌ی نوشتارِ این قانون به نظر نرسه، ولی ساختار وجود داره.

بعد هم همانی از چپ:

-- x به return اعمال
-- برای شروع میده m a یه مقدار

  return x >>= f   =  f  x
--     [1]    [2]      [3]

مشابه ‏‎pure‎‏، تابعِ ‏‎return‎‏ هم نباید تغییری در رفتارِ مابقیِ تابع ایجاد کنه؛ نقش‌ش اینه که چیزهای لازم رو بذاره داخل ساختار، و وجودِ اون ساختار نباید تأثیری در محاسبات داشته باشه.

شرکت‌پذیری

این قانونِ شرکت‌پذیری فرقِ زیادی با بقیه‌ی قانون‌های شرکت‌پذیری که تا اینجا دیدیم نداره. فقط به خاطرِ ذاتِ ‏‎>>=‎‏، یه کم ظاهرش تفاوت داره:

(m >>= f) >>= g  =  m >>= (\x -> f x >>= g)

تغییرِ گروه‌بندیِ توابع نباید تأثیری در جوابِ نهایی داشته باشه، عینِ شرکت‌پذیری ِ ‏‎Monoid‎‏. اونطوری که سمت راستِ تساوی از یه آرگومانِ ‏‎x‎‏ استفاده کردیم، ممکنه در نگاهِ اول یه کم عجیب به نظر برسه. پس دقیق‌تر بررسی‌ش می‌کنیم.

سمتِ چپِ تساوی، همون شکلی‌ه که انتظار داریم:

(m >>= f) >>= g

یادتون باشه که ‏‎(>>=)‎‏ این قابلیت رو داره که مقدارِ خروجی از یه تابع رو به عنوانِ ورودی به تابعِ بعدی بده، شبیهِ اعمالِ تابع با این تفاوت که مقدار در سمتِ چپ، و تابع بعدی سمت راست قرار می‌گیره. این کُد یادتون هست؟

getLine >>= putStrLn

اول اجراییه ِ ‏‎IO‎‏ از ‏‎getLine‎‏ محاسبه میشه، بعد ‏‎String‎‏ ِ حاصل از اجرای اثراتِ ‏‎getLine‎‏ به ‏‎putStrLn‎‏ پاس داده میشه. بخشی از دلیلِ این چپ-به-راست بودن برمی‌گرده به تاریخچه‌ی ‏‎IO‎‏ در هسکل – به این خاطر اینطوریه که "ترتیب" کُد از بالا به پایین خونده بشه. جلوتر در کتاب بیشتر توضیح میدیم.

با شرکت دادن ِ متفاوت، اول باید ‏‎f‎‏ رو اعمال کنیم تا ‏‎g‎‏ یه مقدارِ ورودی از تایپِ ‏‎m a‎‏ داشته باشه و محاسبه رو شروع کنه. پس آرگومان ‏‎x‎‏ رو با یه تابع بی‌نام به ‏‎f‎‏ میدیم:

m >>= (\x -> f x >>= g)

وقت‌ش رسیده

می‌خوایم رحم کنیم، پس دوباره از checkers استفاده می‌کنیم. آرگومانی که باید به ‏‎Monad TestBatch‎‏ بدیم دقیقاً عینِ آرگومانی‌ه که به ‏‎Applicative‎‏ ِش دادیم: یه توپل با سه مقدار در داخلِ ساختار ِ مورد نظر.

Prelude> quickBatch (monad [(1, 2, 3)])

monad laws:
  left  identity: +++ OK, passed 500 tests.
  right identity: +++ OK, passed 500 tests.
  associativity:  +++ OK, passed 500 tests.

جلوتر نمونه‌های ‏‎Monad‎‏ رو با این تست می‌کنیم. اول یه ‏‎Monad‎‏ ِ بد بنویسیم ببینیم چجوری مچ‌مون رو می‌گیره.

‏‎Monad‎‏های بد و زیستگاه‌شون

تو این مثال یه ‏‎Monad‎‏‏‎Functor‎‏) ِ نامعتبر می‌نویسیم. این تایپ رو می‌تونین مثل یه تایپِ ‏‎Identity‎‏ فرض کنین که یه مقدارِ صحیح هم داره که با هر ‏‎fmap‎‏ یا بایند، یکی بهش اضافه میشه.

module BadMonad where

import Test.QuickCheck
import Test.QuickCheck.Checkers
import Test.QuickCheck.Classes

data CountMe a =
  CountMe Integer a 
  deriving (Eq, Show)

instance Functor CountMe where
  fmap f (CountMe i a) =
    CountMe (i + 1) (f a)

instance Applicative CountMe where
  pure = CountMe 0
  CountMe n f <*> CountMe n' a =
    CountMe (n + n') (f a)

instance Monad CountMe where
  return = pure

  CountMe n a >>= f =
    let CountMe _ b = f a
    in CountMe (n + 1) b

instance Arbitrary a
      => Arbitrary (CountMe a) where
  arbitrary =
    CountMe <$> arbitrary <*> arbitrary

instance Eq a => EqProp (CountMe a) where
  (=-=) = eq

main = do
  let trigger :: CountMe (Int, String, Int)
      trigger = undefined
  quickBatch $ functor trigger
  quickBatch $ applicative trigger
  quickBatch $ monad trigger

این تست‌ها رو که اجرا کنیم، ‏‎Functor‎‏ و ‏‎Monad‎‏ از بالا تا پایین شکست می‌خورن. ‏‎Applicative‎‏ در واقع فقط به خاطرِ ‏‎Functor‎‏ شکست می‌خوره؛ در نمونه ِ ‏‎Applicative‎‏ از یه مانوید ِ "ساختار-مناسب" استفاده کردیم.

Prelude> main

functor:
  identity: *** Failed! Falsifiable (after 1 test):
CountMe 0 0
  compose:  *** Failed! Falsifiable (after 1 test):
<function>
<function>
CountMe 0 0

applicative:
  identity:     +++ OK, passed 500 tests.
  composition:  +++ OK, passed 500 tests.
  homomorphism: +++ OK, passed 500 tests.
  interchange:  +++ OK, passed 500 tests.
  functor:
    *** Failed! Falsifiable (after 1 test):
<function>
CountMe 0 0

monad laws:
  left identity:
    *** Failed! Falsifiable (after 1 test):
<function>
0
  right identity:
    *** Failed! Falsifiable (after 1 test):
CountMe 0 0
  associativity:
    *** Failed! Falsifiable (after 1 test):
CountMe 0 0

اگه اون جمعِ عجیب با ۱ رو به ‏‎Applicative‎‏ اضافه کنیم، اون هم خراب میشه:

instance Applicative CountMe where
  pure = CountMe 0
  CountMe n f <*> CountMe n' a =
    CountMe (n + 1) (f a)

حالا همه‌ش خراب‌ه:

applicative:
  identity:
    *** Failed! Falsifiable (after 1 test):
CountMe 0 0
  composition: 
    *** Failed! Falsifiable (after 1 test):
CountMe 0 <function>
CountMe 0 <function>
CountMe 0 0
  homomorphism: 
    *** Failed! Falsifiable (after 1 test):
<function>
0
  interchange: 
    *** Failed! Falsifiable (after 1 test): 
CountMe (-1) <function>
0

حتی اگه نمونه‌های ‏‎Functor‎‏ و ‏‎Applicative‎‏ رو درست کنیم، نمونه ِ ‏‎Monad‎‏ هنوز خراب‌ه.

instance Functor CountMe where
  fmap f (CountMe i a) = CountMe i (f a)

instance Applicative CountMe where
  pure = CountMe 0
  CountMe n f <*> CountMe n' a =
    CountMe (n + n') (f a)

instance Monad CountMe where
  return = pure

  CountMe _ a >>= f = f a

حالا ‏‎Functor‎‏ و ‏‎Applicative‎‏ تست رو صحیح رد می‌کنن، اما این نمونه ِ ‏‎Monad‎‏ معتبر نیست. برای ‏‎Applicative‎‏، اینکه تابعِ ‏‎pure‎‏ مقدارِ ‏‎Integer‎‏ رو صفر میذاره اشکالی نداره، اما این کار قانون همانی از راست برای ‏‎Monad‎‏ رو نقض می‌کنه.

Prelude> CountMe 2 "blah" >>= return
CountMe 0 "blah"

پس ‏‎pure‎‏ خیلی سلیقه‌ای عمل می‌کنه. الان ‏‎Applicative‎‏ و ‏‎Functor‎‏ معتبر اند، اما اگه ‏‎pure‎‏ با ‏‎Monoid‎‏ ِ ساختار سازگاری نداشته باشه چطور؟ مثلاً با تعاریفِ زیر، قوانینِ ‏‎Functor‎‏ رعایت شدن، اما ‏‎Applicative‎‏ معتبر نیست.

instance Functor CountMe where
  fmap f (CountMe i a) = CountMe i (f a)

instance Applicative CountMe where
  pure = CountMe 1
  CountMe n f <*> CountMe n' a =
    CountMe (n + n') (f a)

اگه از همانی ِ مانوید، منطبق با عملیات ِ اون مانوید استفاده کنیم، که در این مورد میشه عدد صفر در کنارِ عملیات جمع، ‏‎Applicative‎‏ دوباره معتبر میشه.

instance Applicative CountMe where
  pure = CountMe 0
  CountMe n f <*> CountMe n' a =
    CountMe (n + n') (f a)

با این ساختار‌ها که بیشتر تجربه کسب کنین، راحت‌تر تشخیص میدین چه چیزی ممکنه ‏‎Applicative‎‏ ِ معتبر داشته باشه ولی هیچ نمونه ِ معتبرِ ‏‎Monad‎‏ نداشته باشه. ولی حالا تو این مسئله نمونه ِ ‏‎Monad‎‏ رو چطور درست کنیم؟ با تعمیرِ ‏‎Monoid‎‏ ِ زیرِش!

instance Monad CountMe where
  return = pure

  CountMe n a >>= f =
    let CountMe n' b = f a
    in CountMe (n + n') b

حالا که نمونه ِ ‏‎Monad‎‏، مشابهِ ‏‎Applicative‎‏، اون شمارنده‌ها رو با هم جمع می‌کنه، دیگه درست کار می‌کنه! آسون پیش میاد که نمونه ِ مونَدی که تعریف می‌کنیم، تایپچِک بشه اما معتبر نباشه، به همین خاطر استفاده از ‏‎QuickCheck‎‏ برای بررسیِ نمونه‌های ‏‎Monoid‎‏، ‏‎Functor‎‏، ‏‎Applicative‎‏، و ‏‎Monad‎‏ مهم‌ه.