3

I'm reading about monad transformers in Real World Haskell. In the following example, the stack is Writer on top State on top of Reader on top of IO.

{-# Language GeneralizedNewtypeDeriving #-}

import Control.Monad
import Control.Monad.State
import Control.Monad.Reader
import Control.Monad.Writer
import System.Directory
import System.FilePath

data AppConfig = AppConfig {
      cfgMaxDepth :: Int
    } deriving Show

data AppState = AppState {
      stDeepestReached :: Int
    } deriving Show

newtype MyApp a = MyA {
      runA :: WriterT [(FilePath,Int)] (StateT AppState (ReaderT AppConfig IO)) a
    } deriving (Monad, MonadIO, Functor, MonadReader AppConfig,
                MonadWriter [(FilePath,Int)], MonadState AppState)

runApp :: MyApp a -> Int -> IO ([(FilePath,Int)], AppState)
runApp k maxDepth = let config = AppConfig maxDepth
                        state' = AppState 0
                     in runReaderT (runStateT (execWriterT $ runA k) state') config

constrainedCount :: Int -> FilePath -> MyApp ()
constrainedCount curDepth path = do
  contents <- liftIO . getDirectoryContents $ path
  cfg <- ask
  let maxDepth = cfgMaxDepth cfg
  tell [(path,curDepth)]
  forM_ (filter (\d' -> d' /= ".." && d' /= ".") contents) $ \d -> do
    let newPath = path </> d
    isDir <- liftIO $ doesDirectoryExist newPath
    when (isDir && curDepth < maxDepth) $ do
         let newDepth = curDepth+1
         st <- get
         when (stDeepestReached st < newDepth) $
             put st { stDeepestReached = newDepth }
         constrainedCount newDepth newPath

main = runApp (constrainedCount 0 "/tmp") 2 >>= print

I (think I) understand how I can simply call ask, get and put since these are defined in the MonadReader, MonadWriter and MonadState typeclasses and there are instances such as MonadWriter (StateT s m) and so on.

What I don't understand is why I cannot explicit lift an action from the layer below up to the current monad transformer. In constrainedCount I'm in the Reader monad, if I understand correctly, and I thought both st <- get and st <- lift get should work. (And that tell and lift . lift . tellshould be the same). If I changest <- gettost <- lift get` I get the error

Couldn't match type `t0 m0' with `MyApp'
Expected type: MyApp ()
Actual type: t0 m0 ()

which tells me very little... Is my understanding of this completely wrong?

beta
  • 2,380
  • 21
  • 38
  • IIRC, `get` doesn't take any parameters, so `lift . get` wouldn't work because `(.)` takes two functions and composes them. Have you tried `lift get` without the composition? – bheklilr Aug 29 '13 at 19:52
  • To (possibly) answer your question, I think it's because it's wrapped in a newtype. I don't know why you'd want to explicitly `lift` actions, though. – bheklilr Aug 29 '13 at 20:05
  • I would't want to do it in “real” code – I simply wanted to do it to test my understanding. – beta Aug 29 '13 at 20:17

1 Answers1

9

Let's have a look at the type of lift get:

lift get :: (MonadTrans t, MonadState a m) => t m a

But your MyApp isn't a monad transformer, it's only a monad. But what's inside is, of course, so if you use

    st <- MyA $ lift get

it works.

Petr
  • 62,528
  • 13
  • 153
  • 317
  • 2
    Ah. Right. That *does* make sense. If I change from `MyApp` to `type App = WriterT [(FilePath,Int)] (StateT AppState (ReaderT AppConfig IO))`, it works as expected and I can use, e.g. `lift get`. – beta Aug 29 '13 at 20:24