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