۱۸ - ۴مثالهایی از کاربرد 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
Nothing
mkSphericalCow'' "" 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