۲۵ - ۹تایپکلاسِ 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
کافی بود.