۱۸ - ۵قوانین موند
تایپکلاس 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
مهمه.