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