This post is mean to explain the differences between the 3. These 3 terms come up regularly and it is important to differentiate them.
This post aims for the pre-intermediate haskell programmers that have already learn the fundamental concept (monad, functor ...) and have written some haskell program already.
monad-transformer is a type container such as
WriterT ... We will focus on the use-case only.
When people refer to
monad-transformer they usually mean something their
newtype AppM a = ReaderT Env (LoggingT IO) a
The reason why haskell application use this stack over just
IO () because
monad-transformer provide extra functionality for example
ask function to get env variable instead of passing it in all the functions.
mtl library define various typeclasses that help working with monad-transformer. Remember that
mtl is a library not a
The reason why mtl is created because when we define a function to work with
monad-transformer stack, we do not want to use
AppM in the function type.
- instead of this
askEnvAndReadFile :: FilePath -> AppM String
- we want to use this
askEnvAndReadFile :: (MonadReader Env m, MonadIO m) -> FilePath -> m String
MonadReader and MonadIO are typeclasses that show that the function can ask for Env and can do IO action. This function can be use within any
monad-trasnformer stack that has ReaderT and IO in them, not just our predefined AppM.
Sometimes when mtl is being discussed, it is not about the mtl library itself but the techniques that mtl library uses which is the Tagless Final. I will not provide detail explanation of this, but basically this is a technique that help haskell programmer write function with business model constraint.
For example, we want to define a function that can manage certain resources (make api call to get the resources). We do not want the function to be able to do any IO action.
Below show a function that can only make http call and log, not any other action
app :: (ManageResource m, HasLogging m) -> m () app = do result <- makeApiCall log (show result) pure ()
HasLogging is a typeclass that has nothing to do with mtl library, but instead we define this by ourself:
class (Monad m) => ManageResource m where makeApiCall :: m Resource
This is just an interface only, the implementation
makeApiCallIO will have
MonadIO constraint or return IO monad:
makeApiCallIO :: IO Resource).
So when we want to use this function with our stack AppM we can define an instance for this
instance ManageResource AppM where makeApiCall = makeApiCallIO
The useful thing of using typeclass constraints instead of concrete type such as
AppM is that we can use a mock stack MockM where we do not actually do any IO action.
makeApiCallMock :: m Resource makeApiCallMock = pure someMockData instance ManageResource MockM where makeApiCall = makeApiCallMock
As we can see, the 3 terms are different. The most important thing is the
mtl-style technique that is used a lot in realworld haskell application (HasDatabase, HasLogging ...). It is important to be able to read other people constraint typeclasses as well as create our own. I was confused to this 3 terms as well before, hope this clears up some misunderstanding.