۲۵ - ۲ترانسفورمرِ Maybe یا MaybeT

در فصلِ قبل، ترانسفورمر ِ ‏‎IdentityT‎‏ رو جزبه‌جز بررسی کردیم. شاید حدس زده باشین، ولی ‏‎IdentityT‎‏ از مفیدترین موند ترانسفورمرها نیست، با این حال بدونِ کاربرد هم نیست (بعداً بیشتر میگیم). مفید‌بودنِ ‏‎Maybe Monad‎‏ رو قبلاً دیدیم، متعاقباً نسخه‌ی ترانسفورمر‌ِش، یعنی ‏‎MaybeT‎‏ هم در کنار بقیه‌ی ترانسفورمر‌های مهم قرار می‌گیره.

ترانسفورمر ِ ‏‎MaybeT‎‏ یه کم پیچیده‌تر از IdentityT ِه. اگه همه‌ی تمرین‌های فصل قبل رو انجام دادین، تو این فصل هم به مشکلی نمی‌خورین، چون این فصل بر پایه‌ی چیزهایی که با تایپِ ‏‎IdentityT‎‏ و ‏‎Compose‎‏ دیدین نوشته شده. برای اطمینان از اینکه ترانسفورمرها کاملاً براتون ابهام‌زدایی بشه، ارزش داره این فصل رو با دقت پیش برین.

با ‏‎newtype‎‏ ِ ترانسفورمر شروع می‌کنیم:

newtype MaybeT m a =
  MaybeT { runMaybeT :: m (Maybe a) }

ساختار ِ تایپِ ‏‎MaybeT‎‏ و ‏‎Compose‎‏ مشابه همدیگه‌اند، پس می‌تونیم از نمونه‌های ‏‎Functor‎‏ و ‏‎Applicative‎‏ ِ تایپِ ‏‎Compose‎‏ کمک بگیریم:

-- یادتون هست؟ Compose برای Functor
instance (Functor f, Functor g)
      => Functor (Compose f g) where
  fmap f (Compose fga) = 
    Compose $ (fmap . fmap) f fga

-- مقایسه کنین MaybeT با نمونه‌ی
instance (Functor m)
      => Functor (MaybeT m) where
  fmap f (MaybeT ma) = 
    MaybeT $ (fmap . fmap) f ma

لازم نیست کار متفاوتی برای نمونه ِ ‏‎Functor‎‏ انجام بدیم چون ترانسفورمرها فقط برای ‏‎Monad‎‏ لازم‌اند، نه ‏‎Functor‎‏.

می‌خوایم لو بدیم!

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

اول با چیزی که به نظر جوابِ ‏‎MaybeT Applicative‎‏ میاد شروع می‌کنیم و می‌بینینم که چرا کار نمی‌کنه. این کامپایل نمیشه:

instance (Applicative m)
      => Applicative (MaybeT m) where
  pure x = MaybeT (pure (pure x))

  (MaybeT fab) <*> (MaybeT mma) = 
    MaybeT $ fab <*> mma

‏‎fab‎‏ تابعِ ‏‎m (Maybe (a -> b))‎‏، و ‏‎mma‎‏ معادلِ یه مقدار با تایپِ ‏‎m (Maybe a)‎‏ هست.

این خطا رو میده:

Couldn't match type ‘Maybe (a -> b)’
with ‘Maybe a -> Maybe b’

برای مقایسه، این نمونه ِ ‏‎Applicative‎‏ برای ‏‎Compose‎‏ ِه:

instance (Applicative f, Applicative g)
      => Applicative (Compose f g) where
  pure x = Compose (pure (pure x))

  Compose f <*> Compose x =
    Compose ((<*>) <$> f <*> x)

این رو حل کرده بودین دیگه... درسته؟ حالا جزبه‌جز بررسی‌ش می‌کنیم.

اینجا باید اول یه اپلای ِ ‏‎Applicative‎‏ رو از روی ساختار ِ ‏‎f‎‏ لیفت کنیم تا اون ‏‎g (a -> b)‎‏ رو به ‏‎g a -> g b‎‏ تغییر بدیم و بتونیم از نمونه ِ ‏‎Applicative‎‏ ِ تایپِ ‏‎f‎‏ بهره ببریم. این ایده رو میشه با تایپ‌های معین هم دید:

innerMost
  :: [Maybe (Identity (a -> b))]
  -> [Maybe (Identity a -> Identity b)]
innerMost = (fmap . fmap) (<*>)

second'
  :: [Maybe (Identity a -> Identity b)]
  -> [ Maybe (Identity a)
    -> Maybe (Identity b) ]
second' = fmap (<*>)

final'
  :: [ Maybe (Identity a)
    -> Maybe (Identity b) ]
  -> [Maybe (Identity a)]
  -> [Maybe (Identity b)]
final' = (<*>)

تابعی که می‌تونه نمونه ِ ‏‎Applicative‎‏ برای چنین تایپِ فرضی‌ای باشه، این شکلی میشه:

lmiApply :: [Maybe (Identity (a -> b))] 
         -> [Maybe (Identity a)]
         -> [Maybe (Identity b)]
lmiApply f x =
  final' (second' (innerMost f)) x

به خاطر بسته بودنِ اپلیکتیو تحتِ ترکیب (که فصلِ قبل هم اشاره کردیم)، ‌نمونه ِ ‏‎Applicative‎‏ برای تایپِ ‏‎MaybeT‎‏ هم با همین ایده تعریف میشه. فقط وقتی برسیم به ‏‎Monad‎‏ باید کارِ متفاوتی نسبت به ‏‎Compose‎‏ انجام بدیم.

instance (Applicative m)
      => Applicative (MaybeT m) where
  pure x = MaybeT (pure (pure x))

  (MaybeT fab) <*> (MaybeT mma) =
    MaybeT $ (<*>) <$> fab <*> mma

نمونه ِ ‏‎MaybeT Monad‎‏

بالاخره رسیدیم به نمونه ِ ‏‎Monad‎‏! بعضی از تایپ‌های میانی هم نوشتیم:

instance (Monad m)
      => Monad (MaybeT m) where
  return = pure

  (>>=) :: MaybeT m a
        -> (a -> MaybeT m b)
        -> MaybeT m b

  (MaybeT ma) >>= f =
--        [2]    [3]
    MaybeT $ do
--  [ 1 ]

      -- ma :: m (Maybe a)
      -- v :: Maybe a
      v <- ma
--      [4]
      case v of
--        [5]
          Nothing -> return Nothing
--        [           6           ]

          Just y -> runMaybeT (f y)
--        [  7  ]   [      8      ]
-- y :: a
-- f :: a -> MaybeT m b
-- f y :: MaybeT m b
-- runMaybeT (f y) :: m (Maybe b)

۱.

در آخر باید یه مقدارِ ‏‎MaybeT‎‏ برگردونیم، پس قبل از بلوک ِ ‏‎do‎‏ یه داده‌ساز ِ ‏‎MaybeT‎‏ گذاشتیم. در نتیجه جوابِ نهایی از بلوک ِ ‏‎do‎‏ باید از تایپِ ‏‎m (Maybe b)‎‏ باشه تا تایپچک بشه، چون می‌خوایم از ‏‎MaybeT m a‎‏ به ‏‎MaybeT m b‎‏ برسیم.

۲.

اولین آرگومانِ بایند، ‏‎MaybeT m a‎‏ هست. با تطبیق الگو روی داده‌ساز ِ نیوتایپ ِ ‏‎MaybeT‎‏، بازش کردیم.

۳.

آرگومانِ دوم بایند، ‏‎(a -> MaybeT m b)‎‏ هست.

۴.

به یک چیز در تعریفِ ‏‎MaybeT‎‏ دقت کنین:

newtype MaybeT m a =
  MaybeT { runMaybeT :: m (Maybe a) }
--                      ^---------^

یه مقدارِ ‏‎Maybe‎‏ زیر یه تایپِ دیگه (که فقط می‌دونیم یه نمونه ِ ‏‎Monad‎‏ داره) پوشونده شده. بر همین مبنا بلوک ِ ‏‎do‎‏ رو با استفاده از گرامر ِ فِلشِ چپ برای بایند شروع می‌کنیم. از این طریق به مقدارِ ‏‎Maybe‎‏ ِ داخلِ ساختار ِ ‏‎m‎‏ دسترسی پیدا می‌کنیم.

۵.

از اونجا که با استفاده از ‏‎<-‎‏، مقدارِ ‏‎Maybe a‎‏ رو از داخلِ ‏‎m (Maybe a)‎‏ به بیرون انقیاد دادیم، می‌تونیم از بیانیه‌ی case با اون مقدارِ ‏‎Maybe‎‏ استفاده کنیم.

۶.

اگه ‏‎Nothing‎‏ بگیریم، ‏‎Nothing‎‏ هم برمی‌گردونیم، فقط باید بذاریم‌ش توی ساختار ِ ‏‎m‎‏. نمی‌دونیم ‏‎m‎‏ چیه، اما چون یه ‏‎Monad‎‏ ِه (و در نتیجه یه ‏‎Applicative‎‏) پس می‌تونیم با استفاده از ‏‎return‎‏ (یا ‏‎pure‎‏) ببریم‌ش زیرِ اون ساختار.

۷.

اگر هم ‏‎Just‎‏ بگیریم، به یه مقدارِ ‏‎a‎‏ دسترسی پیدا می‌کنیم و میشه به عنوانِ آرگومان به تابعِ ‏‎f‎‏ با تایپِ ‏‎a -> MaybeT m b‎‏ بدیم.

۸.

مقدارِ ‏‎m (Maybe b)‎‏ رو باید از ‏‎MaybeT‎‏ خارج کنیم (فولد کنیم)، چون کلِ این بلوک ِ ‏‎do‎‏ زیر یه سازنده ِ ‏‎MaybeT‎‏ قرار داره.

شاید بد نباشه با قلم و کاغذ انقدر روی اینها کار کنین تا طرز کار همه‌شون رو درست درک کنین بعد ادامه بدین.