۲۵ - ۹تایپکلاسِ MonadTrans
زیاد پیش میاد که بخوایم تابعی رو به داخلِ یه بافت ِ بزرگتر لیفت کنیم. مدتی هست که این کار رو با Functor انجام میدیم، که یه تابع رو به داخلِ یه بافت لیفت میکنه و به مقدارِ داخلش اعمال میکنه. این قابلیت مبنای Applicative، Monad، و Traversable هم هست. با این حال fmap همیشه کافی نیست. پس توابعی داریم که اساساً همون fmap برای بافتهای متفاوتاند:
fmap :: Functor f
=> (a -> b) -> f a -> f b
liftA :: Applicative f
=> (a -> b) -> f a -> f b
liftM :: Monad m
=> (a -> r) -> m a -> m rشاید متوجه شده باشین که در اسم دو تابعِ آخر lift هست. با اینکه بهتون گفته بودیم که خیلی درگیرِ اسمِ توابع نشین، اما اینجا اسمِ تابعها یه ایده از کاری که میکنن میده: یه تابع رو به داخلِ یه بافت ِ بزرگتر لیفت میکنن، درست مثلِ fmap. تایپِ زیرینِ تابعِ بایند از Monad هم، یه تابعِ لیفتینگ ِه – دوباره fmap! – که با تابعِ join ترکیب شده.
در بعضی موارد لازم میشه با ساختارهای بیشتر یا متفاوتی از این تایپها کار کنیم. در بقیهی موارد هم چیزی میخوایم که هر چقدر لازمه لیفت کنه تا به بیرونیترین جایگاه (از لحاظِ ساختاری) در یه دسته از موند ترانسفورمرها برسه. موند ترانسفورمرها رو میشه تودرتو کرد تا اثراتِ مختلف ترکیب بشن و به یه تابعِ گنده برسیم، ولی برای مدیریت ِ چنین دستههایی باید بیشتر لیفت کنیم.
تایپکلاسی که لیفت میکنه
تایپکلاسِ MonadTrans فقط یک متود ِ اصلی داره: lift. به طورِ کلی، کارِش لیفت کردنِ اجراییههای Monad به داخلِ یه تایپِ ترانسفورمر ِه. خیلی خوب!
class MonadTrans t where
-- | یک محاسبه رو از موندِ آرگومان
-- | (constructed به موندِ ساختهشده (یا
-- | .لیفت میکنه
lift :: (Monad m) => m a -> t m aاینجا t یه تایپِ موند ترانسفورمر ِه که یه نمونه ِ MonadTrans داره.
حالا یه مثال نسبتاً ساده با scotty رو بررسی میکنیم.
انگیزه برای MonadTrans
scotty یه چارچوبِ وب برای هسکله. نمیخوایم واردِ جزئیاتِ scotty بشیم، اما موند ترانسفورمرهایی که scotty باهاشون کار میکنه، خودشون نیوتایپهایی از دستههای موند ترانسفورمر اند. وایسا، چی شد؟
newtype ScottyT e m a =
ScottyT
{ runS :: State (ScottyState e m) a }
deriving (Functor, Applicative, Monad)
newtype ActionT e m a =
ActionT
{ runAM
:: ExceptT
(ActionError e)
(ReaderT ActionEnv
(StateT ScottyResponse m))
a
}
deriving ( Functor, Applicative )
type ScottyM = ScottyT Text IO
type ActionM = ActionT Text IOما اینجا ActionM و ActionT و ScottyM و ScottyT رو یکسان میدونیم، اما همونطور که از دو خطِ آخرِ کُدِ بالا معلومه، اونهایی که M دارن، تایپهای مستعار برای ترانسفورمرها با تایپهای معین ِ داخلی هستن. با این تایپهای معین، یعنی خطاها (سمت چپِ ExceptT) در ScottyM یا ActionM به عنوانِ Text برمیگردن، و سمتِ راستِ ExceptT، میشه IO. ExceptT نسخهی ترانسفورمر ِ Either ِه، و یه ReaderT و یه StateT هم توی دسته هستن. به عنوانِ یه کاربرِ API ِ scotty، این مکانیزمهای داخلی خیلی برای شما مهم نیستن، اما خوبه که ببینین چه چیزهایی توی اون دستهها قرار گرفتن.
حالا برگردیم به مثالمون. این مثالِ “hello, world” برای scotty به حساب میاد، اما این کُدِ خطا میده:
-- scotty.hs
{-# LANGUAGE OverloadedStrings #-}
module Scotty where
import Web.Scotty
import Data.Monoid (mconcat)
main = scotty 3000 $ do
get "/:word" $ do
beam <- param "word"
putStrLn "hello"
html $
mconcat ["<h1>Scotty, ",
beam,
" me up!</h1>"]یادآوری: با نوشتن اینها در ترمینال، میتونین مثال رو دنبال کنین:
$ stack build scotty
$ stack ghci
Prelude> :l scotty.hsوقتی بارگذاریش میکنین، یه خطای تایپ میگیرین:
Couldn't match expected type
‘Web.Scotty.Internal.Types.ActionT
Data.Text.Internal.Lazy.Text IO a0’
with actual type ‘IO ()’
In a stmt of a 'do' block: putStrLn "hello"
In the second argument of ‘($)’, namely
‘do { beam <- param "word";
putStrLn "hello";
html $ mconcat ["< h1 >Scotty, ", beam, ....] }’دلیل این خطای تایپ اینه که putStrLn تایپِش IO () هست، اما داخلِ یه بلوک ِ do، که اون هم داخلِ get قرار داره نوشته شده، و در نتیجه موندی که اون کُد درونش قرار گرفته، میشه ActionM یا ActionT (م. این get برای scotty ِه، با get که برای State دیدیم فرق داره):
get :: RoutePattern
-> ActionM ()
-> ScottyM ()تایپِ ActionT نهایتاً به IO میرسه، اما اول باید از روی ساختارهای دیگهای لیفت کنیم. با اضافهکردنِ یه واردات شروع به تعمیرِ این کُد میکنیم:
import Control.Monad.Trans.Classبعد اون خطِ putStrLn رو با این کُد عوض میکنیم:
lift (putStrLn "hello")باید کار کنه.
میشه برای این lift که داخلِ اجراییه ِ scotty نوشتیم، اعلامِ تایپ هم اضافه کنیم:
let hello = putStrLn "hello"
(lift :: IO a -> ActionM a) helloببینیم چه میکنه. یه بار دیگه فایل رو بارگذاری کنین و تابعِ main رو صدا بزنین. باید این پیغام رو ببینین:
Setting phasers to stun...
(port 3000) (ctrl-c to quit)در نوارِ آدرس مرورگرِتون بنویسین localhost:3000. به دو چیز توجه کنین: یکی اینکه در نوشتهای که چاپ میشه، چیزی بجای beam نوشته نشده (یعنی beam خالیه)، و اینکه برنامه در ترمینال نوشتهی hello چاپ میکنه. ببینین اگه به آخرِ آدرس یه کلمه اضافه کنین چی میشه:
localhost:3000/beamنوشتهی روی صفحه باید تغییر کنه، توی ترمینال هم باید یه hello ِ دیگه چاپ شه. پارامترِ /:word به واسطهی متغیرِ beam، به داخلِ اون خطِ html در انتهای بلوک ِ do اضافه میشه، hello هم از روی ActionM لیفت شده تا بتونه توی ترمینال چاپ بشه. هر اتفاقی که روی صفحهی وب بیوفته، یه hello چاپ میشه.
با مراحل زیر میتونیم استفاده از lift رو اختصاصی کنیم. لطفاً با اعلام ِ تایپها برای اعمالِ lift در برنامهی scotty که بالاتر نوشتیم ما رو دنبال کنید:
lift :: (Monad m, MonadTrans t)
=> m a -> t m a
lift :: (MonadTrans t)
=> IO a -> t IO a
lift :: IO a -> ActionM a
lift :: IO () -> ActionM ()اونجا به این دلیل تونستیم از (t IO a) به (ActionM a) برسیم چون IO داخلِ ActionM هست.
یه کم ActionM رو دقیقتر بررسی کنیم:
Prelude> import Web.Scotty
Prelude> import Web.Scotty.Trans
Prelude> :info ActionM
type ActionM =
ActionT Data.Text.Internal.Lazy.Text IO
-- Defined in ‘Web.Scotty’با نگاه به نمونه ِ MonadTrans برای ActionT (که ActionM تایپِ مستعارِش ِه)، میشه ببینیم lift چه کاری انجام داد:
instance MonadTrans (ActionT e) where
lift = ActionT . lift . lift . liftاز محاسن اینه که خودِ ActionT بر مبنای سهتا موند ترانسفورمر ِه دیگه تعریف شده. میشه در تعریفش دید:
newtype ActionT e m a =
ActionT {
runAM
:: ExceptT
(ActionError e)
(ReaderT ActionEnv
(StateT ScottyResponse m))
a
} deriving (Functor, Applicative)اول ببینیم اگه بجای lift توی کُد، از تعریفش (برای ActionT) استفاده کنیم کار میکنه یا نه:
{-# LANGUAGE OverloadedStrings #-}
module Scotty where
import Web.Scotty
import Web.Scotty.Internal.Types
(ActionT(..))
import Control.Monad.Trans.Class
import Data.Monoid (mconcat)اون (..) میگه همهی دادهسازهای تایپِ ActionT وارد بشن (بجای اینکه هیچ کدوم یا فقط چندتا بخصوص وارد بشن). حالا خودِ برنامهی scotty:
main = scotty 3000 $ do
get "/:word" $ do
beam <- param "word"
(ActionT . lift . lift . lift)
(putStrLn "hello")
html $
mconcat ["<h1>Scotty, ",
beam,
" me up!</h1>"]اینطوری هم کار میکنه! دقت کنین که دادهساز ِ ActionT رو باید از یه ماژول ِ Internal (م. به معنای "داخلی") وارد میکردیم، چون اونجا "مخفی" شده. سهتا lift داریم، یکی برای هرکدوم از ExceptT، ReaderT، و StateT.
بریم سراغ ExceptT:
instance MonadTrans (ExceptT e) where
lift = ExceptT . liftM Rightبرای اینکه بتونیم ازش در کُدِمون استفاده کنیم، باید این واردات هم اضافه کنیم:
import Control.Monad.Trans.Exceptپس برنامهمون اینطوری میشه:
main = scotty 3000 $ do
get "/:word" $ do
beam <- param "word"
(ActionT
. (ExceptT . liftM Right)
. lift
. lift) (putStrLn "hello")
html $
mconcat ["<h1>Scotty, ",
beam,
" me up!</h1>"]بعد برای ReaderT، یه نگاه به Control.Monad.Trans.Reader در کتابخونه ِ transformers میندازیم:
instance MonadTrans (ReaderT r) where
lift = liftReaderT
liftReaderT :: m a -> ReaderT r m a
liftReaderT m = ReaderT (const m)بنا به دلایلی، تابعِ liftReaderT توسطِ transformers صادر نمیشه، ولی خودمون میتونیم تعریف کنیم. این رو به ماژولِتون اضافه کنین:
import Control.Monad.Trans.Reader
liftReaderT :: m a -> ReaderT r m a
liftReaderT m = ReaderT (const m)بعد برنامهمون این شکلی میشه:
main = scotty 3000 $ do
get "/:word" $ do
beam <- param "word"
(ActionT
. (ExceptT . liftM Right)
. liftReaderT
. lift) (putStrLn "hello")
html $
mconcat ["<h1>Scotty, ",
beam,
" me up!</h1>"]یا بجای liftReaderT میشد این کار رو کنیم:
. (\m -> ReaderT (const m))یا:
(ActionT
. (ExceptT . liftM Right)
. ReaderT . const
. lift
) (putStrLn "hello")حالا اون lift ِ آخر از روی StateT! یادمون هست که تایپِ ActionT به StateT ِ تنبل اشاره داشت، میتونیم این نمونه ِ MonadTrans رو ببینیم:
instance MonadTrans (StateT s) where
lift m = StateT $ \s -> do
a <- m
return (a, s)خوب اول import رو بنویسیم:
import Control.Monad.State.Lazy
hiding (get)لازمه get رو مخفی کنیم چون scotty خودش یه تابعِ get داره و ما نیازی به get از StateT نداریم. این هم واردِ کُدِمون کنیم:
main = scotty 3000 $ do
get "/:word" $ do
beam <- param "word"
(ActionT
. (ExceptT . liftM Right)
. ReaderT . const
. \m -> StateT (\s -> do
a <- m
return (a, s))
) (putStrLn "hello")
html $
mconcat ["<h1>Scotty, ",
beam,
" me up!</h1>"]دقت کنین برای دسترسی به اجراییهی موندیای که داشتیم لیفت میکردیم، بیرون از StateT یه لاندا لازم داشتیم. اینجا دیگه رسیدیم به بیرونیترین جای ممکن، و چون ActionM بیرونیترین تایپِ موندی ِ ActionT رو IO تعریف کرده، دیگه بعد از این همه لیفتکردن putStrLn به خوبی کار میکنه.
به طور معمول یه نمونه ِ MonadTrans هربار فقط از روی یک لایه لیفت میکنه، اما scotty ساختار ِ زیرین رو انتزاعی کرده تا شما چنین مشغلهای نداشته باشین؛ به همین خاطر خودش هر سهتا لیفت رو انجام میده. اینجا توجه به این مورد که لیفتکردن به معنای پوشوندنِ یه بیانیه داخلِ بافتی بزرگتره که هیچ کاری انجام نمیده خیلی مهمه.
نمونههای MonadTrans
حالا دلیل اینکه چرا MonadTrans داریم رو میدونین و یه شِمای کلی از کاری که lift (تنها متود ِ MonadTrans) انجام میده پیدا کردین.
اینجا چندتا مثال از نمونههای MonadTrans آوردیم:
۱.
IdentityT
instance MonadTrans IdentityT where
lift = IdentityT۲.
MaybeT
instance MonadTrans MaybeT where
lift = MaybeT . liftM Just
lift
:: (Monad m)
=> m a -> t m a
(MaybeT . liftM Just)
:: (Monad m)
=> m a -> MaybeT m a
MaybeT
:: m (Maybe a) -> MaybeT m a
(liftM Just)
:: Monad m
=> m a -> m (Maybe a)بطور خلاصه، m a رو به داخل بافت ِ MaybeT لیفت کرده.
الگو ِ کلی که MaybeT از نمونههای MonadTrans نشون میده، اینه که معمولاً تزریقِ ساختار ِ معلوم (برای MaybeT، ساختار ِ معلوم میشه Maybe) از روی یه Monad لیفت میشه. منظور از "تزریقِ ساختار" اکثراً همون return میشه، اما چون اینجا میدونیم ساختار ِ Maybe میخوایم، از Just استفاده میکنیم. پس m a به m (T a) تغییر پیدا میکنه، که T یه تایپِ معین ِه که داریم به داخلِ m a لیفتِش میکنیم. آخر سر هم با دادهساز ِ موند ترانسفورمر مقدارمون رو میبریم داخلِ بافت ِ بزرگتر. این یه خلاصهای از مراحلیه که تایپِ مقدارمون میگذرونه:
v :: Monad m => m a
liftM Just :: Monad m => m a -> m (Maybe a)
liftM Just v :: m (Maybe a)
MaybeT (liftM Just v) :: MaybeT m aببینین میتونین تایپهای این یکی رو تشخیص بدین:
۳.
ReaderT
instance MonadTrans (ReaderT r) where
lift = ReaderT . constحالا چندتا نمونه بنویسین!
تمرینها: بیشتر لیفت کنین
کاری که اینها انجام میدن رو به خاطر داشته باشین، انقدر لیفت کنین تا خسته شین.
۱.
خیال کردین دیگه کاری با EitherT نداریم؟
instance MonadTrans (EitherT e) where
lift = undefined۲.
یا StateT. این یکی بیشتر اذیت میکنه.
instance MonadTrans (StateT s) where
lift = undefinedلیفت ِ زیادی، یعنی کُد ایراد داره
با پوزش از نویسندههای اصلی، اما گاهی اوقات با استفاده از موند ترانسفورمرهای معین و صریح، چنین چیزهایی میبینین:
addSubWidget :: (YesodSubRoute sub master)
=> sub
-> WidgetT sub master a
-> WidgetT sub' master a
addSubWidget sub w =
do master <- liftHandler getYesod
let sr = fromSubRoute sub master
i <- WidgetT $ lift $ lift $ lift
$ lift $ lift $ lift
$ lift get
w' <- liftHandler
$ toMasterHandlerMaybe sr
(const sub) Nothing
$ flip runStateT i $ runWriterT
$ runWriterT $ runWriterT
$ runWriterT $ runWriterT
$ runWriterT $ runWriterT
$ unWidgetT w
let ((((((((a,
body),
title),
scripts),
stylesheets),
style),
jscript),
h),
i') = w'
WidgetT $ do
tell body
lift $ tell title
lift $ lift $ tell scripts
lift $ lift $ lift
$ tell stylesheets
lift $ lift $ lift $ lift
$ tell style
lift $ lift $ lift $ lift $ lift
$ tell jscript
lift $ lift $ lift $ lift $ lift
$ lift $ tell h
lift $ lift $ lift $ lift
$ lift $ lift $ lift $ put i'
return aاینطوری کُد ننویسین. بخصوص بعد از اینطوری کُد نوشتن، نرین یه بلاگ از بدیِ موند ترانسفورمرها بنویسین.
بپوشون، پیش-لیفت کن
خوب حالا چطور از اون تونل وحشت دوری کنیم؟ راههای زیادی هست، اما رایجترین و مطمئنترینشون اینه که دسته ِ Monad رو با نیوتایپ انتزاعی کنیم. بعدش با بهرهگیری از این ارائه، کاراییِ لازم رو به عنوان بخشی از API معرفی میکنیم. یه مثال خوب از این رو میتونیم تو (باز هم) scotty ببینیم.
دوباره یه نگاه به تایپ ActionM که قبلاً معرفی کردیم بندازیم:
Prelude> import Web.Scotty
-- برای خواناییِ بیشترِ تایپها،
-- چندتا ماژول دیگه هم وارد میکنیم.
Prelude> import Data.Text.Lazy
Prelude> :info ActionM
type ActionM =
Web.Scotty.Internal.Types.ActionT Text IO
-- Defined in ‘Web.Scotty’بطورِ پیشفرض، scotty تایپهای زیرین رو مخفی* میکنه چون معمولاً در طولِ برنامهتون کاری باهاشون ندارین. این کاری که scotty کرده کار خوبیه. این طراحی باعث میشه تعاریفِ زیرین بطور پیشفرض مخفی باشن، اما با وارد کردنِ یه ماژول ِ Internal، امکانِ دسترسی به اونها برای مواردِ خاص وجود داره:
Prelude> import Web.Scotty.Internal.Types
-- واردات بیشتر برای تایپهای تمیزتر
Prelude> import Control.Monad.Trans.Reader
Prelude> import Control.Monad.Trans.State.Lazy
Prelude> import Control.Monad.Trans.Except
Prelude> :info ActionT
type role ActionT nominal representation nominal
newtype ActionT e (m :: * -> *) a
= ActionT
{runAM :: ExceptT
(ActionError e)
(ReaderT ActionEnv
(StateT ScottyeResponse m))
a}
instance (Monad m, ScottyError e)
=> Monad (ActionT e m)
instance Functor m
=> Functor (ActionT e m)
instance Monad m
=> Applicative (ActionT e m)م. این مخفیکردن با مخفیکردنِ محتویات یه ماژول در حین واردات (کلیدواژه ِ hiding) متفاوته.
خوبیِ این کار اینه که باعث میشه استفاده از تایپ منجر به شلوغی نشه. نیازی هم به خوندن مقالههایی که به هدفِ تحت تأثیر قراردادن استاد راهنماها نوشته شدن نیست (البته خوندنِ کارهای پیشین پیشنهاد میشه). لیفتِ دستی توی Monad هم میتونه کاهش بده یا حتی حذف کنه. دقت کنین با اینکه بیشتر از یک ترانسفورمر توی ActionM هست، برای پیادهسازیِ یه اجراییه ِ I/O در ActionM، فقط یکبار lift کافی بود.