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