I've written a simple Servant application that stores some information in an SQLite db file. Also, I created a generic function that runs a DB query:
{-# 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
There's also a helper function that converts the DB action into a Servant Handler:
convert :: IO a -> Handler a
convert = Handler . ExceptT . try
withDb :: Db IO a -> Handler a
withDb = convert . runAction
This works and my Handlers respond as expected to HTTP calls, but sometimes they collide with one another and one of the calls attempts to access the SQLite file while it's busy by another call, which results in an exception.
If my understanding is correct, adding a Pool
to my runAction
function should solve this problem. However, it seems that I'm not experienced enough with Haskell, as I can't get all the types to match nicely.
I found a few snippets online (e.g. https://github.com/haskell-servant/example-servant-persistent/blob/master/src/App.hs). After several attempts and few changes that I don't necessarily understand I came up with the following piece of code:
-- 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
This one doesn't compile and I get the following error message:
No instance for (Monad Pool) arising from a do statement
In a stmt of a 'do' block: pool <- createSqlitPool dbFileName 5
Can someone please help me with this and - if possible - recommend valuable readings to get a better understanding of Haskell? I really like the language, got the basics, but I more real-life aspects of it quite overwhelming.
Following the hints presented in the answer I found a solution. It wasn't straightforward at all, so I can't put the entire code here, but the key ideas are here:
1.) It turned out that I can't just "create a Pool
and then use it here and there. A Pool
is contained inside a monad, so I need to use it in a monadic way.
2.) The final solution looks like the snippet presented here:
type DbPool = Pool SqlBacked
instead of ConnectionPool
used in the snippet.Servant
handlers as functions DbPool -> Handler a
.DbPool
in Main.pool
to every function that needs it.It works, but I've got a feeling it's massively overcomplicated. As time progresses I'll take a look if there's a way to simplify it.
I believe your problem is caused by a lack of understanding of monads. There are plenty of tutorials on the web, so allow me to simplify the issue and explain it in the context of your code.
When in Haskell we write x :: Int
, we mean that x
is an integer value. In the definition of x
we can write
x :: Int
x = 1+2
but we can't write
x :: Int
x = do
putStrLn "choose an integer!"
v <- readLn
v -- return v would also be wrong
because the x
above is not an integer, but it is an action that will eventually produce an integer. Haskell separates these two things clearly in types, and forces us to write x :: IO Int
to indicate that.
In your code you write
dbPool :: Pool SqlBackend
Now, Pool SqlBackend
is a value, like Int
. This declaration claims that you will define dbPool
as an expression that will evaluate to a pool. This is not an action that can query the DB and produce the pool at the end, this is the pool itself.
By declaring that type, you have painted yourself into a corner, since it is now impossible to call createSqlitePool dbFileName 5
to obtain the pool: that would be an action, but you promised just a value.
How to solve this? I do not know the persistent
library, so I can't suggest a fix with absolute certainty. Still, I can suggest a few things to try.
Well, let's look at the type of createSqlitePool
itself:
createSqlitePool :: (MonadLoggerIO m, MonadUnliftIO m)
=> Text -> Int -> m (Pool SqlBackend)
Note that the end result is not Pool SqlBackend
, but m (Pool SqlBackend)
. In other words, not a value of type Pool SqlBackend
, but an action that will produce Pool SqlBackend
.
First, you could try using a similar type for your action dbPool
.
dbPool :: (MonadLoggerIO m, MonadUnliftIO m) => m (Pool SqlBackend)
Perhaps you can also choose Db IO
as your monad m
, and simplify everything to
dbPool :: Db IO (Pool SqlBackend)
Then, your code should probably be amended as follows:
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