3
votes

I'm learning about mtl and I wish learn the proper way to create new monads as modules (not as typical application usage).

As a simple example I have written a ZipperT monad (complete code here):

{-# LANGUAGE FlexibleInstances, FunctionalDependencies, MultiParamTypeClasses, GeneralizedNewtypeDeriving #-}
module ZipperT (
  MonadZipper (..)
, ZipperT
, runZipperT
) where

import Control.Applicative
import Control.Monad.State

class Monad m => MonadZipper a m | m -> a where
    pushL :: a -> m ()
    pushR :: a -> m ()
    ...

data ZipperState s = ZipperState { left :: [s], right :: [s] }

newtype ZipperT s m a = ZipperT_ { runZipperT_ :: StateT (ZipperState s) m a }
                        deriving ( Functor, Applicative
                                 , Monad, MonadIO, MonadTrans
                                 , MonadState (ZipperState s))

instance (Monad m) => MonadZipper s (ZipperT s m) where
    pushL x = modify $ \(ZipperState left right) -> ZipperState (x:left) right
    pushR x = modify $ \(ZipperState left right) -> ZipperState left (x:right)
    ...

runZipperT :: (Monad m) => ZipperT s m a -> ([s], [s]) -> m (a, ([s], [s]))
runZipperT computation (left, right) = do
    (x, ZipperState left' right') <- runStateT (runZipperT_ computation) (ZipperState left right)
    return (x, (left', right'))

it's works and I can compose with other monads

import Control.Monad.Identity
import Control.Monad.State
import ZipperT

length' :: [a] -> Int
length' xs = runIdentity (execStateT (runZipperT contar ([], xs)) 0)
    where contar = headR >>= \x -> case x of
                     Nothing -> return ()
                     Just  _ -> do
                                    right2left
                                    (lift . modify) (+1)
                                 -- ^^^^^^^
                                    contar

But I wish to avoid the explicit lift.

  • What is the correct way to create modules like this?
  • Can I avoid the explicit lift? (I wish to hide the internal StateT structure of my ZipperT)

Thank you!

1

1 Answers

3
votes

I think that if you can write an instance of MonadState for your transformer you can use modify without the lift:

instance Monad m => MonadState (ZipperT s m a) where
   ...

I must confess I am not sure about what part of the state modify should affect, though.


I've looked at the complete code. It seems that you already define

MonadState (ZipperState s) (ZipperT s m)

This already provides a modify which however modifies the wrong underlying state. What you actually wanted was to expose the state wrapped in m, provided that is a MonadState itself. This could theoretically be done with

instance MonadState s m => MonadState s (ZipperT s m) where
   ...

But now we have two MonadState instances for the same monad, causing a conflict.


I think I somehow solved this.

Here's what I did:

First, I removed the original deriving MonadState instance. I instead wrote

getZ :: Monad m => ZipperT s m (ZipperState s)
getZ = ZipperT_ get

putZ :: Monad m => ZipperState s -> ZipperT s m ()
putZ = ZipperT_ . put

modifyZ :: Monad m => (ZipperState s -> ZipperState s) -> ZipperT s m ()
modifyZ = ZipperT_ . modify

and replaced previous occurrences of get,put,modify in the ZipperT library with the above custom functions.

Then I added the new instance:

-- This requires UndecidableInstances
instance MonadState s m => MonadState s (ZipperT a m) where
   get = lift get
   put = lift . put

And now, the client code works without lifts:

length' :: [a] -> Int
length' xs = runIdentity (execStateT (runZipperT contar ([], xs)) 0)
    where contar :: ZipperT a (StateT Int Identity) ()
          contar = headR >>= \x -> case x of
                     Nothing -> return ()
                     Just  _ -> do
                                    right2left
                                    modify (+ (1::Int))
                                 -- ^^^^^^^
                                    contar