Search code examples
mongodbhaskellscotty

Haskell scotty Action to IO


I am back again trying to learn Haskell and, oh boy it is difficult! I am a trying to do a simple mongoDB insertion inside a Scotty endpoint. Problem is the type return by the insert function is not accepted in the Scotty do statement. The program is quite simple:

{-# LANGUAGE OverloadedStrings #-}
import Web.Scotty
import Data.Monoid (mconcat)
import Control.Monad.Trans(liftIO,lift,MonadIO)
import System.IO
import Data.Text.Lazy.Encoding (decodeUtf8)
import Data.Text.Lazy (pack,unpack)
import Data.Maybe
import Data.Time.Clock.POSIX
import Database.MongoDB    (Action, Document, Document, Value, access,
                            allCollections,insert, close, connect, delete, exclude, find,
                            host,findOne, insertMany, master, project, rest,
                            select, liftDB, sort, Val, at, (=:))

main :: IO ()
main = scotty 3000 $ do

    post "/logs" $ do
       id <- liftIO $ getTimeInMillis
       b <- body
       let decodedBody = unpack(decodeUtf8 b)
       i <- liftIO $ insertLog id decodedBody
       text $ "Ok"

--setup database connection
run::MonadIO m => Action m a -> m a 
run action = do
        pipe <- liftIO(connect $ host "127.0.0.1")
        access pipe master "data" action

getTimeInMillis ::Integral b => IO b
getTimeInMillis = round `fmap` getPOSIXTime

insertLog::MonadIO m => Int -> String -> Action m Value
insertLog id body = run $ insert "logs" ["id" =: id, "content" =: body]

the problem comes in the line

 i <- liftIO $ insertLog id decodedBody

And the type error is

 Expected type: Web.Scotty.Internal.Types.ActionT
                       Data.Text.Internal.Lazy.Text IO Value
 Actual type: Action m0 Value

Any help or tip will be welcome!


Solution

  • I see a different error message with that code. Maybe you made some changes (like adding liftIO).

    • Couldn't match type ‘Control.Monad.Trans.Reader.ReaderT
                             Database.MongoDB.Query.MongoContext m0 Value’
                     with ‘IO a0’
      Expected type: IO a0
        Actual type: Action m0 Value
    

    In the line:

    i <- liftIO $ insertLog id decodedBody
    

    the liftIO function expects a genuine IO action, of type IO a for some a. However, the expression insertLog id decodedBody doesn't represent an IO action. It is Mongo action of type Action m Value for some m that has a MonadIO constraint. You need to use some function run Mongo Action values in IO. It looks like you've already written such a function, named run. It's written for a general MonadIO m but can be specialized to:

    run :: Action IO a -> IO a
    

    so if you first run your Mongo action (to turn it into IO) and then lift that action (to run it in the Scotty action under post), the following should type check:

    i <- liftIO $ run $ insertLog id decodedBody
    

    Update: Whoops! I missed the run in the insertLog function. You either want to write:

    -- use "run" here
    main = do
       ...
       i <- liftIO $ run $ insertLog id decodedBody
    
    -- but no "run" here
    insertLog::MonadIO m => Int -> String -> Action m Value
    insertLog id body = insert "logs" ["id" =: id, "content" =: body]
    

    OR you want to write:

    -- no "run" here
    main = do
       ...
       i <- liftIO $ insertLog id decodedBody
    
    -- change the type signature and use "run" here
    insertLog :: Int -> String -> IO Value
    insertLog id body = run $ insert "logs" ["id" =: id, "content" =: body]
    

    That will avoid the double-run problem.

    The reason run didn't work as intended in your original code is a little complicated...

    The problem is that run has flexibility to convert its Mongo action to many possible monads by returning m a for any m that supports MonadIO m. Because you gave insertLog a type signature with return type MonadIO m' => Action m' Value (where I changed the variable to keep m and m' distinct), the type checker matched the return type of run to the return type of insertLog:

    m a ~ Action m' Value
    

    by setting a ~ Value and m ~ Action m'. So, your run in insertLog was actually used with the following bizarre type:

    run :: Action (Action m') Value -> Action m' Value
    

    Normally, this would have caused a type error, but the type of insert is also flexible. Instead of returning an action of type Action IO Value, which would be the "usual" type, it happily adapted itself to return an action of type Action (Action IO) Value to match what run was expecting.