Do I have to move hidden packages to project dependencies in a Haskell project just because I want to extract some functionality?

Viewed 63

In Haskell, extracting out some functionality involving a large monad transformer into a separate function requires me to declare a number of hidden packages as project dependencies. This is a pain and takes time. Is there a better way of declaring the type of the extracted functionality, such that I don't have to add packages to my package.yaml dependencies?

Here is the sub-optimal workflow described in detail.

I have a Haskell persistent application that looks like this:

app :: IO ()
app = runStderrLoggingT $ withMySQLConn connectInfo $ runSqlConn $ do
  entries <- selectFirst [UserId ==. 0 ] []
  liftIO $ print entries

I want to extract out the sql connection so I can use it for other queries. So I make this function:

runDb = runStderrLoggingT . withMySQLConn connectInfo . runSqlConn

Now the compiler complains about ambiguous types in the runDb function:

• Ambiguous type variable ‘m0’ arising from a use of ‘runStderrLoggingT’
      prevents the constraint ‘(MonadIO m0)’ from being solved.
...
• Ambiguous type variable ‘m0’ arising from a use of ‘withMySQLConn’
      prevents the constraint ‘(unliftio-core-0.2.0.1:Control.Monad.IO.Unlift.MonadUnliftIO
                                  m0)’ from being solved.

So I comment out runDb fire up the REPL with stack ghci and query the type: :t runStderrLoggingT . withMySQLConn connectInfo . runSqlConn, to which ghci tells me:

runStderrLoggingT . withMySQLConn connectInfo . runSqlConn
  :: unliftio-core-0.2.0.1:Control.Monad.IO.Unlift.MonadUnliftIO m =>
     transformers-0.5.6.2:Control.Monad.Trans.Reader.ReaderT
       SqlBackend (LoggingT m) a
     -> m a

So I now write my runDb function with a type definition:

runDb :: MonadUnliftIO m => ReaderT SqlBackend (LoggingT m) a -> m a
runDb = runStderrLoggingT . withMySQLConn connectInfo . runSqlConn

But now stack complains that it cannot find type constructors MonadUnliftIO, ReaderT, and LoggingT. So I have to make sure I import them:

import Control.Monad.Trans.Reader (ReaderT)
import Control.Monad.IO.Unlift (MonadUnliftIO)
import Control.Monad.Logger    (LoggingT, runStderrLoggingT)

However, this doesn't work, since Control.Monad.Trans.Reader and Control.Monad.IO.Unlift are hidden packages. So I have to add them to my dependencies in package.yaml, and now everything works.

Whew! This seems like a long process. Is there a better way of handling the ambiguous type problem that arose when I extracted the runDb function?

1 Answers

The ambiguity warning is probably caused by the monomorphism restriction. You could use NoMonomorphismRestriction or maybe changing from point-free to pointful style also helps, but I don't think these are good solutions.

You really should add type signatures to top level definitions like runDb and for that you will need to import the types that you use in that signature.

You can perhaps make the process slightly easier by using a type wildcard:

runDb :: _
runDb = runStderrLoggingT . withMySQLConn connectInfo . runSqlConn

Then GHC will give you the required type in an error message, so you won't need to use the repl.

You could even use HLS, which has suggestions that allow you to add the type signature and imports with a few clicks, but you will have to manually add the dependencies to the package.yaml.

I don't want to make light of your troubles, but I don't think this process should take more than about a minute and you will probably not need to do this very often, so I wouldn't worry about optimizing this workflow so much.

Related