2

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.

LA.27
  • 1,888
  • 19
  • 35

1 Answers1

1

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
chi
  • 111,837
  • 3
  • 133
  • 218
  • Thanks for the response. Two things: 1.) You said "I believe your problem is caused by a lack of understanding of monads." <- that's totally true, I need to improve in this field. 2.) I quickly gave a go to your suggestions, but still no good. I'll try again in the evening, thanks for the explanation so far and the direction. – LA.27 Nov 03 '21 at 13:47
  • 1
    Btw. If I understand correctly what you said, `runAction` will create a new pool for each call and if I want to use a shared pool, then I need to put everything into a single Monad, right? – LA.27 Nov 03 '21 at 20:08
  • 1
    @AlojzyLeszcz I don't know how persistent works so I can't really say. Still, if you only need one pool for the whole computation, you need to use something like `do pool <- createPool ... ; use pool ; use_again pool ; ...` and avoid calling `createPool` more than once. – chi Nov 03 '21 at 20:27
  • I see, thanks for explanation. One last question: can you recommend a good book on Haskell? I've read "Learn You...", and now I'm searching for new valuable readings. – LA.27 Nov 03 '21 at 20:45
  • 1
    @AlojzyLeszcz I am unsure about what to recommend. LYAH is nice, but you read that. We had Real World Haskell, but now it's a bit outdated. Maybe you should simply read about some monad tutorial (there are countless) and do some practice with basic tasks using e.g. `IO`. A nice monad page I like is https://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html – chi Nov 03 '21 at 22:04
  • Thanks, I'll give it a go. Btw. I've found a solution - thanks for the hints! – LA.27 Nov 04 '21 at 00:17