Haskell 我应该使用ReaderT在Servant中传递数据库连接池吗?

Haskell 我应该使用ReaderT在Servant中传递数据库连接池吗?,haskell,monad-transformers,servant,Haskell,Monad Transformers,Servant,我正在构建一个带有服务和持久性的web API。我计划定义一些API端点(大约15个),它们使用连接池来访问DB 例如,端点定义(Handlers)之一是: getUser::ConnectionPool->Int->Handler User getUser池uid=do 用户捕获“id”Int:> (获取“[JSON]a :ReqBody'[JSON]a:>Post'[JSON]NoContent ) 类型UserPoint=点“用户”用户 用户服务器::连接池->服务器用户点 用户服务器池u

我正在构建一个带有服务和持久性的web API。我计划定义一些API端点(大约15个),它们使用连接池来访问DB

例如,端点定义(
Handler
s)之一是:

getUser::ConnectionPool->Int->Handler User
getUser池uid=do
用户捕获“id”Int:>
(获取“[JSON]a
:ReqBody'[JSON]a:>Post'[JSON]NoContent
)
类型UserPoint=点“用户”用户
用户服务器::连接池->服务器用户点
用户服务器池uid=
getUser池uid:
姿势池
我将
main
定义为:

main=runstdoutlogging。使用PostgreSqlPool连接字符串numConnections$\pool->do
withResource池(runSqlConn$runMigration migrateAll)
liftIO$run appPort(用户服务器池)
但我很快就注意到,我必须将池逐层(在上面的示例中有2层,在我的实际项目中有3层)传递给每个函数(超过20层)。我的直觉告诉我这是一种难闻的气味,但我不太确定

然后我想到了
ReaderT
,因为我认为这可能会将池抽象出来。但我担心的是,
ReaderT
的引入可能会导致不必要的复杂性:

  • 我需要手动举起很多东西
  • 类型的心智模式将变得更加复杂,因此更难思考
  • 这意味着我必须放弃
    处理程序
    类型,这也使得使用仆人变得更加困难
我不确定在这种情况下是否应该使用
ReaderT
。请提供一些建议(如果您能提供一些关于何时使用
ReaderT
或其他monad转换器的指导,我将不胜感激)

更新:我发现我可以使用
where
-子句来简化这个过程,这基本上解决了我的问题。但我不确定这是否是最佳实践,所以我仍在寻找答案

userServer::Pooled(服务器用户点)
userServer池auth=c:rud其中
c::UserCreation->Handler NoContent
c=未定义
rud uid=r:u:d其中
处理程序用户
r=do
checkAuth池验证
用户处理程序NoContent
u=未定义
d::Handler NoContent
d=未定义

在与服务器一起定义处理程序时,将避免参数传递,因为服务器变得越来越复杂,您可能希望单独定义一些处理程序:

  • 也许某些处理程序提供了一些通用功能,在其他服务器中可能很有用

  • 共同定义一切意味着一切都意识到其他一切。 将处理程序移动到顶层,甚至移动到另一个模块,将 帮助明确他们真正需要知道的部分。 这可以使处理程序更容易理解

一旦我们分离了一个处理程序,就需要为它提供环境。这可以通过函数的普通参数或
ReaderT
来完成。随着参数数量的增加,
ReaderT
(通常与辅助类型类结合使用)变得更具吸引力,因为它使您不必关心参数顺序

我必须一层一层地向下传递池(在示例中) 上面有2层,在我的真实项目中有3层) 每项功能

除了必须传递参数的额外负担(可能是不可避免的),我认为还有一个潜在的更糟糕的问题:您正在通过多层函数来处理低级细节(连接池)。这可能很糟糕,因为:

  • 您正在将整个应用程序提交给使用实际数据库的应用程序。如果在测试期间,您希望将其与某种内存存储库切换,会发生什么情况

  • 如果需要更改持久化的方式,重构将在应用程序的所有层中产生反响,而不是保持本地化

这些问题的一个可能解决方案是:层N+1上的函数不应作为参数接收连接池,而应接收它们从层N使用的函数。层N上的那些函数已经部分应用于连接池

一个简单的例子:如果您有一些高级逻辑
transferUser::Conn->Handle->IO()
,其中包括对函数
readUserFromDb::Conn->IO User
writeUserToFile::Handle->User->IO()
的硬连线调用,请将其更改为
transferUser::IO User->(User->IO)->IO()

注意,N级的辅助函数可以存储在
ReaderT
上下文中;级别N+1的函数可以从那里获得它们


这意味着我必须放弃处理程序类型,这使得使用 仆人也更努力了

您可以使用
ReaderT
转换器通过
处理程序定义服务器,然后将其传递给函数,该函数将“缩减”到可运行的服务器:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
import Servant
import Servant.API
import Control.Monad.Trans.Reader

type UserAPI1 = "users" :> Capture "foo" Int :> Get '[JSON] Int

data Env = Env

-- also valid type
--      server1 :: Int -> ReaderT Env Handler Int
server1 :: ServerT UserAPI1 (ReaderT Env Handler)
server1 = 
    \ param -> 
        do _ <- ask
           return param

-- also valid types:
--      server2 :: ServerT UserAPI1 Handler
--      server2 :: Int -> Handler Int
server2 :: Server UserAPI1 
server2 = hoistServer (Proxy :: Proxy UserAPI1) (flip runReaderT Env) server1  
{-#语言数据类型}
{-#语言类型运算符{-}
进口佣人
导入服务程序API
导入控制.Monad.Trans.Reader
键入UserAPI1=“users”:>捕获“foo”Int:>Get'[JSON]Int
数据环境=环境
--也是有效类型
--server1::Int->ReaderT Env Handler Int
server1::ServerT UserAPI1(ReaderT Env处理程序)
服务器1=
\参数->
do uuhandler Int
服务器2::服务器用户API1
server2=提升服务器(Proxy::Proxy UserAPI1)(flip runReaderT Env)server1

将其封装为一元状态?@bipll您是指
状态吗?@XyRen使用
where
方法避免必须传递pool参数的一个问题是,它强制您定义
用户服务器
中的所有处理程序。在…上