Search code examples
sqlitehaskellpoolservant

Haskell, Sqlite, Pools and Servant


Background

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.

Question: how to add Pool to my app?

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.

EDIT

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:

  • Use a type alias type DbPool = Pool SqlBacked instead of ConnectionPool used in the snippet.
  • Declare all the Servant handlers as functions DbPool -> Handler a.
  • Create a DbPool in Main.
  • Pass the created 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.


Solution

  • 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