Skip to content

zongwu's blog

Scotty web framework 源码初探

scotty 是一款非常容易上手的、构建在 WAI/Wrap 之上的 Haskell web frameowrk,官方文档的 start 例子:

{-# LANGUAGE OverloadedStrings #-}
import Web.Scotty

import Data.Monoid (mconcat)

main = scotty 3000 $
    get "/:word" $ do
        beam <- param "word"
        html $ mconcat ["<h1>Scotty, ", beam, " me up!</h1>"]

编译运行即可体验简单的 web 应用。

放到末尾看 scotty类型,先从简单的入手。

添加 Route

增加不同的 url route 和对应的 handler方法如下:

{-# LANGUAGE OverloadedStrings #-}
import Web.Scotty

main = scotty 3000 $
    get "/foo" $ do
      ...
    get "/bar" $ do
        ...

查看文档:

scotty :: Port -> ScottyM () -> IO ()
get :: RoutePattern -> ActionM () -> ScottyM ()

这里就有点奇怪了,get方法需要两个参数 RoutePatternActionM () 然后返回 ScottyM ()。 多次调用 get方法每次都返回 ScottyM ()scotty使用的只是最后一个 ScottyM (),那中间设置的那些ScottyM ()咧?

我们继续深入看get方法的实现。

get = addroute GET
addroute :: StdMethod -> RoutePattern -> ActionM () -> ScottyM ()
addroute = Trans.addroute

继续看源码

addroute :: (ScottyError e, MonadIO m) => StdMethod -> RoutePattern -> ActionT e m () -> ScottyT e m ()
addroute method pat action = ScottyT $ MS.modify $ \s -> addRoute (route (routeOptions s) (handler s) (Just method) pat action) s

到这一步就能解惑了,每一次使用get方法,都会 modify ScottyT e m ()(也就是 scottyM() ),上面的方法实现里:

route :: (ScottyError e, MonadIO m) => RouteOptions -> ErrorHandler e m -> Maybe StdMethod -> RoutePattern -> ActionT e m () -> Middleware m
addRoute :: Middleware m -> ScottyState e m -> ScottyState e m
addRoute r s@(ScottyState {routes = rs}) = s { routes = r:rs }
\s -> addRoute (route ...) s

这个 lambda 中的 s::ScottyState e m。完美。

类似的还有:

post :: RoutePattern -> ActionM () -> ScottyM ()
put :: RoutePattern -> ActionM () -> ScottyM ()
delete :: RoutePattern -> ActionM () -> ScottyM ()
patch :: RoutePattern -> ActionM () -> ScottyM ()
options :: RoutePattern -> ActionM () -> ScottyM ()

RoutePattern

{-# LANGUAGE OverloadedStrings #-}
...
get "/foo/:bar" $ ...

其实就是:

get (capture "/foo/:bar") $ ...

使用了OverloadedStrings 扩展不需要显式地写 capture。源码:

instance IsString RoutePattern where
    fromString = Capture . pack

RoutePattern 的定义:

data RoutePattern = Capture   Text
                  | Literal   Text
                  | Function  (Request -> Maybe [Param])

除了上面的 capture URL 匹配方式,还支持正则匹配:

regex :: String -> RoutePattern

复杂的方法匹配:

function :: (Request -> Maybe [Param]) -> RoutePattern

以及精确匹配:

literal :: String -> RoutePattern

middleware

middleware :: Middleware -> ScottyM ()
middleware = Trans.middleware

Trans.middleware的定义:

middleware :: Middleware -> ScottyT e m ()
middleware = ScottyT . modify . addMiddleware

addMiddleware :: Wai.Middleware -> ScottyState e m -> ScottyState e m
addMiddleware m s@(ScottyState {middlewares = ms}) = s { middlewares = m:ms }

获取请求参数

request :: ActionM Request
request = Trans.request


-- Trans.request
-- | Get the 'Request' object.
request :: Monad m => ActionT e m Request
request = ActionT $ liftM getReq ask

比较简单,这里注意下 liftM,提升一个函数到 monad m

liftM :: Monad m => (a1 -> r) -> m a1 -> m r

getReq就是 data ActionEnv的获取getReq的函数咯:

data ActionEnv = Env { getReq       :: Request
                     , getParams    :: [Param]
                     , getBody      :: IO ByteString
                     , getBodyChunk :: IO BS.ByteString
                     , getFiles     :: [File]
                     }
newtype ActionT e m a = ActionT { runAM :: ExceptT (ActionError e) (ReaderT ActionEnv (StateT ScottyResponse m)) a }
    deriving ( Functor, Applicative, MonadIO )

这里有点复杂,我们知道ask :: m r ,这是个多态函数,所以具体是什么类型?

我们注意到:

request :: Monad m => ActionT e m Request
request = ActionT $ ...

这里的ActionT显然是数据构造子 (data constructor),它的类型是 ExceptT (ActionError e) (ReaderT ActionEnv (StateT ScottyResponse m)) a -> ActionT e m a

那么 liftM getReq ask的类型就是 ExceptT (ActionError e) (ReaderT ActionEnv (StateT ScottyResponse m)) a 又结合 request::ActionT e m Request 所以:

liftM getReq ask :: ExceptT (ActionError e) (ReaderT ActionEnv (StateT ScottyResponse m)) Request

liftM getReq ask 的计算顺序是 先ask 然后再 liftM getReq

我们知道 liftM :: Monad m => (a1 -> r) -> m a1 -> m r, 不改变 m 而是修改 m里的 a1,那么:

ask :: ExceptT (ActionError e) (ReaderT ActionEnv (StateT ScottyResponse m)) a

我们先用a 代表 ask :: m r 中的 r 。我们当然猜测它是ActionEnv,但是还需要严密地推断。

这时候,就需要一个额外知识:

instance MonadReader r m => MonadReader r (ExceptT e m) where
    ask   = lift ask
    local = mapExceptT . local
    reader = lift . reader

ExceptT e m 实现里 MonadReader r m,所以我们关注的 ask 就是 ExceptT (ActionError e) (ReaderT ActionEnv (StateT ScottyResponse m)) aask

MonadReader r (ExceptT e m)ask = lift ask

结合 lift :: (MonadTrans t, Monad m) => m a -> t m a 的定义, m a 提升(lift)到了 t m a

这里就是: (ReaderT ActionEnv (StateT ScottyResponse m)) a 提升(lift)到了 ExceptT (ActionError e) (ReaderT ActionEnv (StateT ScottyResponse m)) a

(ReaderT ActionEnv (StateT ScottyResponse m))ask 我们熟悉, ask :: Monad m => ReaderT r m r 也就是 (ReaderT ActionEnv (StateT ScottyResponse m)) ActionEnv 把这个结构liftExceptT ... 最终得到:

ask :: ExceptT (ActionError e) (ReaderT ActionEnv (StateT ScottyResponse m)) ActionEnv

呼呼。

至于 liftM getReq ask的类型就得到 liftM getReq ask :: ExceptT (ActionError e) (ReaderT ActionEnv (StateT ScottyResponse m)) Request

然后再结合上最前面的ActionT 得到 request :: ActionT e m Request

此外,我们在源码里有:

instance (Monad m, ScottyError e) => Monad.Monad (ActionT e m) where
    return = ActionT . return
    ActionT m >>= k = ActionT (m >>= runAM . k)
#if !(MIN_VERSION_base(4,13,0))
    fail = Fail.fail
#endif

instance ScottyError e => MonadTrans (ActionT e) where
    lift = ActionT . lift . lift . lift

instance (MonadReader r m, ScottyError e) => MonadReader r (ActionT e m) where
    {-# INLINE ask #-}
    ask = lift ask
    {-# INLINE local #-}
    local f = ActionT . mapExceptT (mapReaderT (mapStateT $ local f)) . runAM

这里稍微注意一下 return = ActionT . retrun 。左边的 return Monad.Monad (ActionT e m) 的,右边的return:

return a = ExceptT (ActionError e) (ReaderT ActionEnv (StateT ScottyResponse m)) a

然后再用ActionT包装了一下。

>>= 操作也是类似,实质上是先将 ActionT脱去做计算,最后再包上ActionT

类似的还有:

header :: Text -> ActionM (Maybe Text)
headers :: ActionM [(Text, Text)]
body :: ActionM ByteString

bodyReader :: ActionM (IO ByteString)

param :: Parsable a => Text -> ActionM a
params :: ActionM [Param]

jsonData :: FromJSON a => ActionM a
files :: ActionM [File]

修改Response

status :: Status -> ActionM ()
addHeader :: Text -> Text -> ActionM ()
setHeader :: Text -> Text -> ActionM ()
redirect :: Text -> ActionM a

设置 Response

text :: Text -> ActionM ()
html :: Text -> ActionM ()
file :: FilePath -> ActionM ()
json :: ToJSON a => a -> ActionM ()
stream :: StreamingBody -> ActionM ()
raw :: ByteString -> ActionM ()

Exception

raise :: Text -> ActionM a
raiseStatus :: Status -> Text -> ActionM a
rescue :: ActionM a -> (Text -> ActionM a) -> ActionM a
next :: ActionM a
finish :: ActionM a
defaultHandler :: (Text -> ActionM ()) -> ScottyM ()
liftAndCatchIO :: IO a -> ActionM a

Scotty

type ScottyM = ScottyT Text IO
type ActionM = ActionT Text IO

-- | Run a scotty application using the warp server.
scotty :: Port -> ScottyM () -> IO ()
scotty p = Trans.scottyT p id

跟进 Trans.scottyT 看看

-- | Run a scotty application using the warp server.
-- NB: scotty p === scottyT p id
scottyT :: (Monad m, MonadIO n)
        => Port
        -> (m Response -> IO Response) -- ^ Run monad 'm' into 'IO', called at each action.
        -> ScottyT e m ()
        -> n ()
scottyT p = scottyOptsT $ def { settings = setPort p (settings def) }

主要是设置 settings 的端口。

还有个 def :: forall a. Default a => a 主要是获取 a 的默认值 。

-- | Run a scotty application using the warp server, passing extra options.
-- NB: scottyOpts opts === scottyOptsT opts id
scottyOptsT :: (Monad m, MonadIO n)
            => Options
            -> (m Response -> IO Response) -- ^ Run monad 'm' into 'IO', called at each action.
            -> ScottyT e m ()
            -> n ()
scottyOptsT opts runActionToIO s = do
    when (verbose opts > 0) $
        liftIO $ putStrLn $ "Setting phasers to stun... (port " ++ show (getPort (settings opts)) ++ ") (ctrl-c to quit)"
    liftIO . runSettings (settings opts) =<< scottyAppT runActionToIO s

这里要注意 =<< 操作符:

=<< :: forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b

回忆一下 >>= 的类型:

>>= ::  forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b

最后就是 scottyAppT

-- | Turn a scotty application into a WAI 'Application', which can be
-- run with any WAI handler.
-- NB: scottyApp === scottyAppT id
scottyAppT :: (Monad m, Monad n)
           => (m Response -> IO Response) -- ^ Run monad 'm' into 'IO', called at each action.
           -> ScottyT e m ()
           -> n Application
scottyAppT runActionToIO defs = do
    let s = execState (runS defs) def
    let rapp req callback = runActionToIO (foldl (flip ($)) notFoundApp (routes s) req) >>= callback
    return $ foldl (flip ($)) rapp (middlewares s)

runS 的定义:

newtype ScottyT e m a = ScottyT { runS :: State (ScottyState e m) a }
    deriving ( Functor, Applicative, Monad )

let s = execState (runS defs) def 的结果 s :: ScottyState e m 它的默认值定义:

instance Default (ScottyState e m) where
    def = ScottyState [] [] Nothing def
----- Transformer Aware Applications/Middleware -----
type Middleware m = Application m -> Application m
type Application m = Request -> m Response

--------------- Scotty Applications -----------------
data ScottyState e m =
    ScottyState { middlewares :: [Wai.Middleware]
                , routes :: [Middleware m]
                , handler :: ErrorHandler e m
                , routeOptions :: RouteOptions
                }
notFoundApp :: Monad m => Scotty.Application m
notFoundApp _ = return $ responseBuilder status404 [("Content-Type","text/html")]
                       $ fromByteString "<h1>404: File Not Found!</h1>"

这里比较复杂点的代码是:

    let rapp req callback = runActionToIO (foldl (flip ($)) notFoundApp (routes s) req) >>= callback

先看

foldl (flip ($)) notFoundApp (routes s) req

这里是以 notFoundApp 为初始值,取 routes s :: [Middleware m] 数组里的第一个值,结合 flip ($) 元算, 就是 Middleware m $ notFoundApp 结果依然是一个 Application m 毕竟 Middleware m 作为中间件只是包裹了一下 Application m 不改变其类型。得到结果后,依次取数组的值一直执行,这里就是把所有的 routes s 都挂载到 初始的 Application m 上(notFoundApp就是初始的 App ,对所有的请求都返回 404)

我们知道 type Application m = Request -> m Response 然后给一个 req 之后,会返回 m response

然后再由 runActionToIO 执行,最后 >>= callback

注意 rapp :: Request -> (Response -> IO b) -> IO b等下回头看。

整个函数最后的结果:

    return $ foldl (flip ($)) rapp (middlewares s)

这里也是用类似的 foldl[Wai.Middleware] 应用到 rapp 上。

这里要注意,middlewares s :: [Wai.Middleware]

Wai.Middleware的定义:

type Middleware = Application -> Application
type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived.

可以看出来刚刚提到的 rapp 就是 Wai.Applicationcallback :: Response -> IO ResponseReceived