۱۸ - ۴مثالهایی از کاربرد Monad
خیلی خوب، تا اینجا فرقِ Monad رو دیدیم، یه مثال هم از کاربردش دیدیم. حالا طرزِ کارِ موندها با موندهایی غیر از IO رو در کُد ببینیم.
لیست
برای این تایپکلاسها مثالها رو با لیست شروع کردیم چون درکشون معمولاً خیلی راحتتره. با این حال سعی میکنیم این بخش کوتاه باشه چون چیزای جذابتر داریم که نشونتون بدیم.
اختصاصیسازی تایپها
دیگه تا الان با این پروسه آشنا شدین:
(>>=) :: Monad m
=> m a -> (a -> m b) -> m b
(>>=) :: [ ] a -> (a -> [ ] b) -> [ ] b
-- با گرامر رایجتر
(>>=) :: [a] -> (a -> [b]) -> [b]
-- pure عینِ
return :: Monad m => a -> m a
return :: a -> [ ] a
return :: a -> [a]عالی. شبیهِ fmap میمونه که آرگومانهاش جابجا شدن، و اینکه میتونیم داخلِ تابعِ نگاشت شدهمون لیستهای بیشتر (یا یه لیست خالی) ایجاد کنیم. بریم باهاش یه دوری بزنیم.
مثالی از Monad ِ لیست در عمل
با یه تابع شروع میکنیم:
twiceWhenEven :: [Integer] -> [Integer]
twiceWhenEven xs = do
x <- xs
if even x
then [x*x, x*x]
else [x*x]اون خطِ x <- xs مقادیرِ مجرد از لیستِ ورودی (مثل یه لیست توصیفی) میگیره و یه a بهمون میده. اون if-then-else، همون a -> m b میشه. مقادیرِ مجردِ a که از داخلِ m a مقیّد شدن رو میگیره و میتونه مقادیرِ بیشتر ایجاد، و در نتیجه سایزِ لیست رو افزایش بده.
آرگومانی که در کُدِ زیر دادیم، همون m a ِه (اولین ورودیمون):
Prelude> twiceWhenEven [1..3]
[1,4,4,9]حالا این رو امتحان کنین:
twiceWhenEven :: [Integer] -> [Integer]
twiceWhenEven xs = do
x <- xs
if even x
then [x*x, x*x]
else []همون ورودی رو بدین که مقایسهش راحتتر بشه. انتظارِ جوابش رو داشتین؟ بازم باهاش تو REPL بازی کنین، فرضیه درست کنین و تست کنین تا طرزِ کارِ موندها رو در یه مثالِ ساده درک کنین. مثالهای بخشهای بعد طولانیتر و پیچیدهتر میشن.
Maybe Monad
حالا رسیدیم به یکی از کارهای جذابتری که با این تواناییِ جدید میشه انجام بدیم.
اختصاصیسازی تایپها
-- type M = Maybe
-- m ~ Maybe
(>>=) :: Monad m
=> m a -> (a -> m b) -> m b
(>>=) ::
Maybe a -> (a -> Maybe b) -> Maybe b
-- pure عینِ
return :: Monad m => a -> m a
return :: a -> Maybe aنباید چیزی براتون جدید بوده باشه، پس بریم سر اصل مطلب.
استفاده از Maybe Monad
این مثال شبیه مثال از فصلِ اپلیکتیو ِه، اما فرق داره. با اینکه صراحتاً همه چیز رو گفتیم، پیشنهاد میکنیم دوتا مثال رو با هم مقایسه کنین. بالاتر با گرامر ِ do و Monad ِ لیست آشنا شدین؛ اینجا همهی اتفاقات رو صراحتاً توضیح دادیم، به Either که برسیم همهشون باید روشن شده باشن. خوب شروع کنیم:
data Cow = Cow {
name :: String
, age :: Int
, weight :: Int
} deriving (Eq, Show)
noEmpty :: String -> Maybe String
noEmpty "" = Nothing
noEmpty str = Just str
noNegative :: Int -> Maybe Int
noNegative n | n >= 0 = Just n
| otherwise = Nothing
-- بود، باید Bess اگه اسم گاو
-- باشه 500 کمتر از
weightCheck :: Cow -> Maybe Cow
weightCheck c =
let w = weight c
n = name c
in if n == "Bess" && w > 499
then Nothing
else Just c
mkSphericalCow :: String
-> Int
-> Int
-> Maybe Cow
mkSphericalCow name' age' weight' =
case noEmpty name' of
Nothing -> Nothing
Just nammy ->
case noNegative age' of
Nothing -> Nothing
Just agey ->
case noNegative weight' of
Nothing -> Nothing
Just weighty ->
weightCheck
(Cow nammy agey weighty)Prelude> mkSphericalCow "Bess" 5 499
Just (Cow {name = "Bess", age = 5, weight = 499})
Prelude> mkSphericalCow "Bess" 5 500
Nothingاول به کمک گرامر ِ do تروتمیزِش میکنیم، بعد هم میبینیم که چرا نمیشه این کار رو با Applicative انجام بدیم:
-- .نیست IO فقط برای do گرامر
mkSphericalCow' :: String
-> Int
-> Int
-> Maybe Cow
mkSphericalCow' name' age' weight' = do
nammy <- noEmpty name'
agey <- noNegative age'
weighty <- noNegative weight'
weightCheck (Cow nammy agey weighty)و همونطور که انتظار میره کار میکنه:
Prelude> mkSphericalCow' "Bess" 5 500
Nothing
Prelude> mkSphericalCow' "Bess" 5 499
Just (Cow {name = "Bess", age = 5, weight = 499})با (>>=) هم میشه بنویسیمش؟ حتماً!
-- .تلنبار کردن لانداهای تودرتو
mkSphericalCow'' :: String
-> Int
-> Int
-> Maybe Cow
mkSphericalCow'' name' age' weight' =
noEmpty name' >>=
\nammy ->
noNegative age' >>=
\agey ->
noNegative weight' >>=
\weighty ->
weightCheck (Cow nammy agey weighty)حالا چرا نمیشه این کار رو با Applicative انجام بدیم؟ به خاطر اینکه تابعِ weightCheck به یه مقدارِ Cow لازم داره و در تایپِ خروجیش، Maybe Cow، ساختار موندی ِ بیشتری برمیگردونه. اگه کُدتون با گرامر ِ do این شکلی بشه:
doSomething = do
a <- f
b <- g
c <- h
pure (a, b, c)میشه با Applicative بازنویسیش کرد. در مقابل اگه چنین چیزی دارین:
doSomething' n = do
a <- f n
b <- g a
c <- h b
pure (a, b, c)موند لازم دارین، چون g و h برمبنای مقادیری ساختار موندی درست میکنن که اون مقادیر (م. در اینجا، دو انقیاد ِ a و b) خودشون وابسته به مقادیریاند که از یه ساختار موندی (م. در اینجا، نتیجههای حاصل از f n و g a) ایجاد شدن. برای یکی کردنِ ساختارهای موندی ِ تودرتو به join احتیاج دارین. اگه باورتون نمیشه، سعی کنین doSomething' رو با Applicative بنویسین: یعنی از >>= و join استفاده نکنین.
یه کم کُد که باهاش بازی کنیم:
f :: Integer -> Maybe Integer
f 0 = Nothing
f n = Just n
g :: Integer -> Maybe Integer
g i =
if even i
then Just (i + 1)
else Nothing
h :: Integer -> Maybe String
h i = Just ("10191" ++ show i)
doSomething' n = do
a <- f n
b <- g a
c <- h b
pure (a, b, c)خلاصهی ماجرا:
۱.
با Maybe Applicative، همهی محاسباتِ Maybe مستقل از همدیگه یا شکست میخورن یا موفق میشن. توابعی هم که از روی مقادیرِ Maybe لیفت میکنین ممکنه Just یا Nothing باشن.
۲.
با Maybe Monad، محاسباتی که در نتیجهی آخر نقش دارن، برمبنای محاسباتِ قبلیشون میتونن Nothing برگردونن.
انفجار یه گاو ِ کُرَوی
ادعا کردیم هر اتفاقی اون بالا بیوفته رو کامل و صریح توضیح بدیم، الان به قولمون عمل میکنیم. تهوتوی قضیه رو دربیاریم ببینیم بایند روی مقادیرِ Maybe چطور کار میکنه.
این نمونهای که مثال زدیم (در لحظهی این نوشتار) همینطوری در کتابخونه ِ base ِ GHC نوشته شده:
instance Monad Maybe where
return x = Just x
(Just x) >>= k = k x
Nothing >>= _ = Nothing
mkSphericalCow'' :: String
-> Int
-> Int
-> Maybe Cow
mkSphericalCow'' name' age' weight' =
noEmpty name' >>=
\nammy ->
noNegative age' >>=
\agey ->
noNegative weight' >>=
\weighty ->
weightCheck (Cow nammy agey weighty)اگه چندتا آرگومان بهش بدیم چی میشه؟
-- از بیرونیترین به داخلیترین پیش میریم
mkSphericalCow'' "Bess" 5 499 =
noEmpty "Bess" >>=
\nammy ->
noNegative 5 >>=
\agey ->
noNegative 499 >>=
\weighty ->
weightCheck (Cow nammy agey weighty)
-- ، پس این الگو رو رد میکنیم"Bess" /= ""
-- noEmpty "" = Nothing
noEmpty "Bess" = Just "Bess"پس مقدار Just "Bess" رو درست کردیم؛ اما nammy فقط String میشه، یعنی بدونِ ساختار ِ Maybe چون >>=، به تابعی که روی مقدارِ موندی بایند میکنه ورودیِ a رو میده، نه m a.* با بررسی نمونه ِ Maybe Monad دلیلش رو بیشتر بررسی میکنیم:
instance Monad Maybe where
return x = Just x
(Just x) >>= k = k x
Nothing >>= _ = Nothing
noEmpty "Bess" >>= \nammy ->
(مابقی محاسبات)
-- .محاسبه شد Just "Bess" به noEmpty "Bess"
(Just "Bess") >>= \nammy -> ...
(Just x) >>= k = k x
-- است \nammy -> ... همون تابعِ k
-- .ه، به تنهایی "Bess" فقط xم. به عبارت دیگه، اون تابعی که آرگومانِ دومِ >>= ِه، فقط به مقدارِ داخلِ ساختار ِ آرگومانِ اول (در اینجا "Bess") اعمال میشه.
پس nammy به "Bess" مقیّد شده، و کُدِ زیر، کلِ k ِه:
\"Bess" ->
noNegative 5 >>=
\agey ->
noNegative 499 >>=
\weighty ->
weightCheck (Cow nammy agey weighty)بعد بررسی سن چطور پیش میره؟
mkSphericalCow'' "Bess" 5 499 =
noEmpty "Bess" >>=
\"Bess" ->
noNegative 5 >>=
\agey ->
noNegative 499 >>=
\weighty ->
weightCheck (Cow "Bess" agey weighty)
-- میگیریم Just 5 صادقه، پس 5 >= 0
noNegative 5 | 5 >= 0 = Just 5
| otherwise = Nothingباز هم با اینکه noNegative مقدارِ Just 5 رو برمیگردونه، تابع bind مقدار ۵ رو پاس میده:
mkSphericalCow'' "Bess" 5 499 =
noEmpty "Bess" >>=
\"Bess" ->
noNegative 5 >>=
\5 ->
noNegative 499 >>=
\weighty ->
weightCheck (Cow "Bess" 5 weighty)
-- میگیریم Just 499 صادقه، پس 499 >= 0
noNegative 499 | 499 >= 0 = Just 499
| otherwise = Nothingمقدار ۴۹۹ رو پاس بدیم جلو:
mkSphericalCow'' "Bess" 5 499 =
noEmpty "Bess" >>=
\"Bess" ->
noNegative 5 >>=
\5 ->
noNegative 499 >>=
\499 ->
weightCheck (Cow "Bess" 5 499)
weightCheck (Cow "Bess" 5 499) =
let 499 = weight (Cow "Bess" 5 499)
"Bess" = name (Cow "Bess" 5 499)
-- False == 499 > 499 ،جهت اطلاع
in if "Bess" == "Bess" && 499 > 499
then Nothing
else Just (Cow "Bess" 5 499)پس درنهایت Just (Cow "Bess" 5 499) رو برمیگردونیم.
شکست سریع، مثل یه استارتاپِ پرسرمایه
اگه شکست میخوردیم چطور؟ محاسبهی زیر رو تشریح میکنیم:
Prelude> mkSphericalCow'' "" 5 499
NothingmkSphericalCow'' "" 5 499 =
noEmpty "" >>=
\nammy ->
noNegative 5 >>=
\agey ->
noNegative 499 >>=
\weighty ->
weightCheck (Cow nammy agey weighty)
-- منطبق میشه Nothing پس حالت ،"" == ""
noEmpty "" = Nothing
-- noEmpty str = Just strبعد از محاسبهی noEmpty "" و رسیدن به جواب Nothing، از (>>=) استفاده میکنیم. چطور میشه؟
instance Monad Maybe where
return x = Just x
(Just x) >>= k = k x
Nothing >>= _ = Nothing
-- noEmpty "" := Nothing
Nothing >>=
\nammy ->
-- .منطبق نمیشه، پس ردِش میکنیم Just حالت
-- (Just x) >>= k = k x
-- .از این استفاده میکنیم
Nothing >>= _ = Nothingپس به محضِ اینکه هرکدوم از توابعی که در Maybe Monad نقش دارن یه مقدار Nothing برمیگردونن، تابعِ بایند مابقی محاسبات رو میندازه زمین:
mkSphericalCow'' "" 5 499 =
Nothing >>= -- .نخیراین رو میشه با مقدارِ تهی ثابت کرد:
Prelude> Nothing >>= undefined
Nothing
Prelude> Just 1 >>= undefined
*** Exception: Prelude.undefinedاما چرا از Applicative و Monad برای Maybe استفاده میکنیم؟ چون این کُد:
mkSphericalCow' :: String
-> Int
-> Int
-> Maybe Cow
mkSphericalCow' name' age' weight' = do
nammy <- noEmpty name'
agey <- noNegative age'
weighty <- noNegative weight'
weightCheck (Cow nammy agey weighty)خیلی خوشایندتر از اینه که صدبار روی Just و Nothing تطبیق الگو کنیم، و پشتِسرِهم تکرار کنیم Nothing -> Nothing. زندگی خیلی کوتاهه که بخوایم به کارهای تکراری بگذرونیم، تازه کامپیوترها هم عاشقِ انجام کارهای تکراریاند.
Either
آخیش... خدا رو شکر که تو شیکمِ اون گاوه فقط مقادیرِ Maybe بود. با نشون دادنِ کاربردِ Either Monad ادامه میدیم؛ با درکی که از Maybe پیدا کردین این رو هم راحتتر درک میکنین.
اختصاصیسازی تایپها
طبق معمول، این هم از تایپها:
-- m ~ Either e
(>>=) :: Monad m
=> m a
-> (a -> m b)
-> m b
(>>=) :: Either e a
-> (a -> Either e b)
-> Either e b
-- pure عینِ
return :: Monad m => a -> m a
return :: a -> Either e aچرا هِی این کار رو میکنیم؟ که یادتون بمونه وقتی تایپها رو تشخیص بدین، همیشه راه رو نشون میدن.
استفاده از Either Monad
با چیزهایی که یاد گرفتین، این کُد رو با دقت بخونین. اول نوعدادههامون رو تعریف میکنیم:
module EitherMonad where
-- تعداد سال گذشته
type Founded = Int -- founded ~ م. تاسیس شده
-- تعداد برنامهنویسها
type Coders = Int
data SoftwareShop =
Shop {
founded :: Founded
, programmers :: Coders
} deriving (Eq, Show)
data FoundedError =
NegativeYears Founded
| TooManyYears Founded
| NegativeCoders Coders
| TooManyCoders Coders
| TooManyCodersForYears Founded Coders
deriving (Eq, Show)حالا چندتا تابع اضافه میکنیم:
validateFounded
:: Int
-> Either FoundedError Founded
validateFounded n
| n < 0 = Left $ NegativeYears n
| n > 500 = Left $ TooManyYears n
| otherwise = Right n
validateCoders
:: Int
-> Either FoundedError Coders
validateCoders n
| n < 0 = Left $ NegativeCoders n
| n > 5000 = Left $ TooManyCoders n
| otherwise = Right n
mkSoftware
:: Int
-> Int
-> Either FoundedError SoftwareShop
mkSoftware years coders = do
founded <- validateFounded years
programmers <- validateCoders coders
if programmers > div founded 10
then Left $
TooManyCodersForYears
founded programmers
else Right $ Shop founded programmersدقت کنین که Either سَرِ اولین مقداری که شکست بخوره، اتصال کوتاه میکنه. باید اینطور باشه، چون در Monad، مقادیرِ بعدی ممکنه به مقادیرِ قبلی وابسته باشن:
Prelude> mkSoftware 0 0
Right (Shop {founded = 0, programmers = 0})
Prelude> mkSoftware (-1) 0
Left (NegativeYears (-1))
Prelude> mkSoftware (-1) (-1)
Left (NegativeYears (-1))
Prelude> mkSoftware 0 (-1)
Left (NegativeCoders (-1))
Prelude> mkSoftware 500 0
Right (Shop {founded = 500, programmers = 0})
Prelude> mkSoftware 501 0
Left (TooManyYears 501)
Prelude> mkSoftware 501 501
Left (TooManyYears 501)
Prelude> mkSoftware 100 5001
Left (TooManyCoders 5001)
Prelude> mkSoftware 0 500
Left (TooManyCodersForYears 0 500)خوب، تایپِ Validation هیچ Monad ای نداره. نمونههای Applicative و Monad باید رفتارِ یکسان داشته باشن. چنین چیزی معمولاً اینطوری بیان میشه:
import Control.Monad (ap)
(<*>) == apاین در واقع میگه اگه اپلای ِ Applicative برای یه تایپ، از بایند ِ نمونه ِ Monad بدست اومده باشه، نباید رفتارش تغییر کنه.
-- با توجه به اینها
(<*>) :: Applicative f
=> f (a -> b) -> f a -> f b
ap :: Monad m
=> m (a -> b) -> m a -> m bتابع (<*>) از Applicative رو از نمونه ِ قویتر بدست بیاریم:
ap :: Monad m => m (a -> b) -> m a -> m b
ap m m' = do
x <- m
x' <- m'
return (x x')مشکل اینجاست که نمیشه یه Monad برای Validation درست کرد که مثلِ Applicative خطاها رو جمع کنه (انباشته کنه). در عوض هر نمونه ِ Monad برای Validation، دقیقاً عینِ نمونه ِ Monad برای Either میشه.
تمرین کوتاه: Either Monad
Either Monad رو بنویسین (م. نمونه ِ Monad برای Either).
data Sum a b =
First a
| Second b
deriving (Eq, Show)
instance Functor (Sum a) where
fmap = undefined
instance Applicative (Sum a) where
pure = undefined
(<*>) = undefined
instance Monad (Sum a) where
return = undefined
(>>=) = undefined