Haskell、Sqlite、池和仆人

Haskell, Sqlite, Pools and Servant

背景

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

{-# 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 操作转换为 Servant Handler 的辅助函数:

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

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

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

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

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

我在网上找到了一些片段(例如 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 然后到处使用它。一个 Pool 包含在一个 monad 中,所以我需要使用它以一种单一的方式。 2.) 最终解决方案看起来像呈现的片段 here:

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

我认为您的问题是由于对 monad 缺乏了解造成的。网上有很多教程,所以请允许我简化问题并结合您的代码进行解释。

当在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不是一个整数,而是一个action最终会产生一个整数。 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.

action

首先,您可以尝试为您的 action 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