首页 > 解决方案 > Haskell、Sqlite、池和仆人

问题描述

背景

我编写了一个简单的 Servant 应用程序,它将一些信息存储在 SQLite db 文件中。另外,我创建了一个运行数据库查询的通用函数:

{-# LANGUAGE OverloadedStrings #-}

type Db m a = ReaderT SqlBackend (NoLoggingT (ResourceT m)) a

dbFileName :: Text
dbFileName = "./myDb.db"

runAction :: MoinadUnliftIO m => Db m a -> m a
runAction = runSqlite dbFileName

还有一个辅助函数将 DB 操作转换为仆人处理程序:

convert :: IO a -> Handler a
convert = Handler . ExceptT . try

withDb :: Db IO a -> Handler a
withDb = convert . runAction

这有效,我的处理程序按预期响应 HTTP 调用,但有时它们相互冲突,并且其中一个调用尝试访问 SQLite 文件,而另一个调用正忙,这会导致异常。

如果我的理解是正确的,Pool在我的runAction函数中添加一个应该可以解决这个问题。但是,似乎我对 Haskell 的经验不够,因为我无法让所有类型都很好地匹配。

问题:如何将 Pool 添加到我的应用程序中?

我在网上找到了一些片段(例如https://github.com/haskell-servant/example-servant-persistent/blob/master/src/App.hs)。经过几次尝试和一些我不一定理解的更改后,我想出了以下代码:

-- slightly different now
type Db m a = ReaderT SqlBackend m a

dbPool :: Pool SqlBackend
dbPool = do
  pool <- createSqlitePool dbFileName 5
  runSqlPool (runMigration migrateAll) pool
  pool

runAction :: MoinadUnliftIO m => Db m a -> m a
runAction action = runSqlPool action dbPool

这个没有编译,我收到以下错误消息:

No instance for (Monad Pool) arising from a do statement
In a stmt of a 'do' block: pool <- createSqlitPool dbFileName 5

有人可以帮我解决这个问题 - 如果可能的话 - 推荐有价值的阅读材料以更好地理解 Haskell?我真的很喜欢这门语言,掌握了基础知识,但我对它的更多现实生活方面相当不知所措。

编辑

按照答案中提供的提示,我找到了解决方案。这一点都不简单,所以我不能把整个代码放在这里,但关键思想在这里:

1.)事实证明,我不能只是“创建一个Pool然后在这里和那里使用它。APool包含在一个单子中,所以我需要以单子的方式使用它。2.)最终的解决方案看起来像此处显示的片段:

它有效,但我感觉它过于复杂了。随着时间的推移,我会看看是否有办法简化它。

标签: sqlitehaskellpoolservant

解决方案


我相信您的问题是由于对单子缺乏了解造成的。网上有很多教程,所以请允许我简化问题并在您的代码上下文中进行解释。

当我们在 Haskell 中写x :: Int时,我们的意思是这x是一个整数值。在定义中x我们可以写

x :: Int
x = 1+2

但我们不能写

x :: Int
x = do
   putStrLn "choose an integer!"
   v <- readLn
   v                 -- return v would also be wrong

因为x上面不是整数,而是最终会产生整数的动作。Haskell 在类型上清楚地区分了这两件事,并迫使我们写x :: IO Int出来表明这一点。

在你写的代码中

dbPool :: Pool SqlBackend

现在,Pool SqlBackend是一个值,例如Int. 该声明声称您将定义dbPool为将评估为池的表达式。这不是一个可以查询数据库并最终生成池的操作,这池本身。

通过声明那个类型,你把自己画到了一个角落,因为现在不可能调用createSqlitePool dbFileName 5来获取池:这将是一个动作,但你只承诺了一个值。

如何解决这个问题?我不知道persistent图书馆,所以我不能绝对肯定地建议修复。不过,我可以建议一些尝试。

好吧,让我们看一下createSqlitePool自身的类型:

createSqlitePool :: (MonadLoggerIO m, MonadUnliftIO m) 
                 => Text -> Int -> m (Pool SqlBackend)

请注意,最终结果不是Pool SqlBackend,而是m (Pool SqlBackend)。换句话说,不是类型的值Pool SqlBackend,而是将产生的动作Pool SqlBackend

首先,您可以尝试为您的操作 使用类似的类型dbPool

dbPool :: (MonadLoggerIO m, MonadUnliftIO m) => m (Pool SqlBackend)

也许您也可以选择Db IO作为您的 monad m,并将所有内容简化为

dbPool :: Db IO (Pool SqlBackend)

然后,您的代码可能应该修改如下:

dbPool :: Pool SqlBackend
dbPool = do
  pool <- createSqlitePool dbFileName 5
  runSqlPool (runMigration migrateAll) pool
  return pool                       -- note the "return"

runAction :: MonadUnliftIO m => Db m a -> m a
runAction action = do
   pool <- dbPool         -- run the action, get the value
   runSqlPool action pool

推荐阅读