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:
- 使用类型别名
type DbPool = Pool SqlBacked
而不是代码段中使用的 ConnectionPool
。
- 将所有
Servant
处理程序声明为函数 DbPool -> Handler a
。
- 在 Main 中创建一个
DbPool
。
- 将创建的
pool
传递给每个需要它的函数。
它有效,但我感觉它过于复杂了。随着时间的推移,我会看看是否有简化它的方法。
我认为您的问题是由于对 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
背景
我编写了一个简单的 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:
- 使用类型别名
type DbPool = Pool SqlBacked
而不是代码段中使用的ConnectionPool
。 - 将所有
Servant
处理程序声明为函数DbPool -> Handler a
。 - 在 Main 中创建一个
DbPool
。 - 将创建的
pool
传递给每个需要它的函数。
它有效,但我感觉它过于复杂了。随着时间的推移,我会看看是否有简化它的方法。
我认为您的问题是由于对 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 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