I'm currently refactoring some Haskell code that I have that interacts with Data.Time
. Ultimately I have a bunch of functions that interact with time:
getCurrentTime :: IO UTCTime
getCurrentTime = T.getCurrentTime
getCurrentDay :: IO Day
getCurrentDay = T.utctDay <$> getCurrentTime
daysUntil :: Day -> IO Integer
daysUntil day = T.diffDays day <$> getCurrentDay
etc etc and so on, ultimately these are just my own helper functions that are all based around T.getCurrentTime
from Data.Time
. Which is the 'effect' of all of these functions.
The first refactor I did to this code was to simply change them to use MonadIO
to allow them to be used in the various transformer stacks compatible with this typeclass:
getCurrentTime :: MonadIO m => m UTCTime
getCurrentTime = liftIO T.getCurrentTime
getCurrentDay :: MonadIO m => m Day
getCurrentDay = T.utctDay <$> getCurrentTime
daysUntil :: MonadIO m => Day -> m Integer
daysUntil day = T.diffDays day <$> getCurrentDay
This is straightforward enough as I just need to lift T.getCurrentTime
and the rest of the implementations just follow suit.
Recently though I have been reading about stubbing and faking effects in Haskell, and would like to be able to run these functions with a fake UTCTime
result for getCurrentTime
.
Going off some of the things I have read online, and looking at how Pandoc implements separating out pure and effectful operations, I've come up with this:
newtype TimePure a = TimePure
{ unTimePure :: Reader UTCTime a
} deriving (Functor, Applicative, Monad, MonadReader UTCTime)
newtype TimeEff m a = TimeEff
{ unTimeIO :: m a
} deriving (Functor, Applicative, Monad, MonadIO)
class (Functor m, Applicative m, Monad m) => TimeMonad m where
getCurrentTime :: m UTCTime
instance TimeMonad TimePure where
getCurrentTime = ask
instance MonadIO m => TimeMonad (TimeEff m) where
getCurrentTime = liftIO T.getCurrentTime
getCurrentDay :: TimeMonad m => m Day
getCurrentDay = T.utctDay <$> getCurrentTime
daysUntil :: TimeMonad m => Day -> m Integer
daysUntil day = T.diffDays day <$> getCurrentDay
Again, other than the additional definitions at the top, I haven't had to change much - my original functions just need to change to use TimeMonad m
rather than MonadIO m
.
This is ideal, and I am able to run my time functions in a pure context now.
However now when I come to some real world code, given an example function like this that interacts with the DB:
markArticleRead :: MonadIO m => Key Article -> SqlPersistT m ()
markArticleRead articleKey =
updateLastModified articleKey =<< getCurrentTime
I have to adjust my function like so:
markArticleRead :: (MonadIO m, TimeMonad m) => Key Article -> SqlPersistT m ()
markArticleRead articleKey =
updateLastModified articleKey =<< lift getCurrentTime
Obviously I have to do this as getCurrentTime
does not need MonadIO
to run. The issue I have is with the re-introduction of lift, this is needed because there is two 'layers' of the transformer stack, rather than one (I think thats an appropriate explaination?).
One of the nice things about the introduction of MonadIO
was it removed the need for having to lift things everywhere, and it made functions like this, which a lot of the time contain business logic etc, a lot less noisy. Is there a way for me to re-gain this benefit, where I can get mtl style implicit lifting, or is it impossible now due to the types I have introduced?
Your problem is TimeEff
, its just not needed. The interface separation is type classes, not concrete Monads. TimePure
is good because you need some Monad to provide facilities for testing, but since any old MonadIO
can do the trick for the IO case, you just don't need to specific a concrete Monad for that.
TimeEff
as you have it adds only one thing to your program, and it's the need to use lift
to convert a TimeEff m
to m
. And since this works forall MonadIO
we can use UndecidableInstances
to permit unification without even adding TimeMonad
to the effectful case. (I know UndecidableInstances
sounds bad, but it's not)
instance (Monad m, MonadIO m) => TimeMonad m where
getCurrentTime = liftIO T.getCurrentTime
markArticleRead :: MonadIO m => Key Article -> SqlPersistT m ()
markArticleRead articleKey =
updateLastModified articleKey =<< getCurrentTime
Some other notes.
class (Functor m, Applicative m, Monad m) => TimeMonad m where
can be
class Monad m => TimeMonad m where
since Monad
already has Applicative
and Functor
as superclasses. So those come along for free. Now as a matter of personal taste, I would even leave out Monad
class GetsTime m where
getCurrentTime :: m UTCTime
This kind of decoupling is nice, both because it makes your code more general, but also because it removes any relationship to algebras. The class here really has no laws, and is just not algebraic, so it's nice to leave those relationships open imho. This would mean you need to add annotations in some places, but I feel it's a good thing to document the algebraic constraint and effectful constrain separately.
getCurrentDay :: (Functor m, TimeMonad m) => m Day
getCurrentDay = T.utctDay <$> getCurrentTime