haskellconduit

composition of lift's in MT stack


I am having a lot of trouble understanding this compilation error. The code is the following:

#!/usr/bin/env stack                                                                                                                                                              
-- stack script --resolver lts-8.22                                                                                                                                               
{-# LANGUAGE OverloadedStrings #-}                                                                                                                                                

import           Network.HTTP.Simple                                                                                                                                              
import qualified Data.ByteString       as BS                                                                                                                                      
import           Control.Monad.Reader                                                                                                                                             
import           Control.Monad.Except                                                                                                                                             
import           Control.Monad.Trans.Class                                                                                                                                        
import           Data.Conduit                                                                                                                                                     



type Cmd i o = ReaderT (BS.ByteString) (ExceptT HttpException (ConduitM i o IO))                                                                                                  

runCmd :: Cmd i o a -> BS.ByteString -> ConduitM i o IO (Either HttpException a)                                                                                                  
runCmd cmdTS host = runExceptT $ runReaderT cmdTS host                                                                                                                            


readHost :: Cmd () BS.ByteString ()                                                                                                                                               
readHost = do                                                                                                                                                                     
host <- ask                                                                                                                                                                     
let req = setRequestMethod "GET"                                                                                                                                                
        $ setRequestHost host                                                                                                                                                   
        $ defaultRequest                                                                                                                                                        
lift . lift $ httpSource req getResponseBody  

Basically I have a MT stack of 4 layers and in readHost I am trying to lift an object from the second to bottom layer to the top layer so I have two lift composed together. But sadly the code will not compile and I got the following error:

[-Wdeferred-type-errors]                                                                                                                                                          
• No instance for (Control.Monad.Trans.Resource.Internal.MonadResource                                                                                                        
                     IO)                                                                                                                                                      
    arising from a use of ‘httpSource’                                                                                                                                        
• In the second argument of ‘($)’, namely                                                                                                                                     
    ‘httpSource req getResponseBody’                                                                                                                                          
  In a stmt of a 'do' block:                                                                                                                                                  
    lift . lift $ httpSource req getResponseBody                                                                                                                              
  In the expression:                                                                                                                                                          
    do host <- ask                                                                                                                                                            
       let req                                                                                                                                                                
             = setRequestMethod "GET" $ setRequestHost host $ defaultRequest                                                                                                  
       lift . lift $ httpSource req getResponseBody

Please help me understand what this error message means. On a side note, I have not seen people building a stack like this with ConduiT sitting at the bottom, so I guess this is bad practice? What will be a good "design pattern" here? I know that the word "design patter" does not exist in haskell but I cannot think of a better word. Thanks a lot!


Solution

  • I started by reducing the layers in the monad transformer stack to shrink your code to this:

        #!/usr/bin/env stack
        -- stack script --resolver lts-8.22
        {-# LANGUAGE OverloadedStrings #-}
    
        import           Network.HTTP.Simple
        import qualified Data.ByteString           as BS
        import           Control.Monad.Reader
        import           Control.Monad.Except
        import           Control.Monad.Trans.Class
        import           Data.Conduit
    
        type Cmd i o = ConduitM i o IO
    
        readHost :: BS.ByteString -> Cmd () BS.ByteString ()
        readHost host = do
          let req = setRequestMethod "GET" . setRequestHost host $ defaultRequest
          httpSource req getResponseBody
    

    I then ran stack {scriptname} and got an error very similar to the one you got for the larger stack:

        {scriptname}:17:3: error:
            • No instance for (resourcet-1.1.9:Control.Monad.Trans.Resource.Internal.MonadResource
                                 IO)
                arising from a use of ‘httpSource’
            • In a stmt of a 'do' block: httpSource req getResponseBody
              In the expression:
                do { let req
                           = setRequestMethod "GET" . setRequestHost host $ defaultRequest;
                     httpSource req getResponseBody }
              In an equation for ‘readHost’:
                  readHost host
                    = do { let req = ...;
                           httpSource req getResponseBody }
    

    Figuring out this smaller case may very well make the more complicated version easier to tackle.

    Let's look at the type for httpSource:

        httpSource :: (MonadResource m, MonadIO n) =>
                         Request
                      -> (Response (ConduitM i ByteString n ()) -> ConduitM i o m r)
                      -> ConduitM i o m r
    

    Its return type is ConduitM i o m r where m must be an instance of MonadResource.

    Now let's have a second glance at the final return type of readHost. It's Cmd () BS.ByteString () which is nothing but ConduitM () BS.ByteString IO () in disguise.

    Compare Conduit () BS.ByteString IO () with ConduitM i o m r to see that m corresponds with IO.

    Is IO an instance of MonadResource? According to GHC, it isn't.

    A little looking around using Hoogle lets us know that ResourceT IO is an instance of MonadResource. Let's try it out in a program.

        -- stack script --resolver lts-8.22
        {-# LANGUAGE OverloadedStrings #-}
    
        import           Network.HTTP.Simple
        import qualified Data.ByteString           as BS
        import           Control.Monad.Reader
        import           Control.Monad.Except
        import           Control.Monad.Trans.Class
        import           Control.Monad.Trans.Resource
        import           Data.Conduit
    
        main :: IO ()
        main = putStrLn "hello"
    
        type Cmd i o = ConduitM i o (ResourceT IO)
    
        readHost :: BS.ByteString -> Cmd () BS.ByteString ()
        readHost host = do
          let req = setRequestMethod "GET" . setRequestHost host $ defaultRequest
          httpSource req getResponseBody
    

    This compiles without errors and also prints to standard output.

    I believe you can use this fix for the larger program. At the very least it explains the error.