۲۵ - ۲ترانسفورمرِ 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 <*> mmafab تابعِ 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 قرار داره.
شاید بد نباشه با قلم و کاغذ انقدر روی اینها کار کنین تا طرز کار همهشون رو درست درک کنین بعد ادامه بدین.