[ANN] type safe diffUTCTime

Both arguments of diffUTCTime function from time package have the same type. It is easy to mix them.

f = do
  started <- getCurrentTime
  threadDelay 10_000_000
  ended <- getCurrentTime
  pure $ started `diffUTCTime` ended

This package provides a stricter diffUTCTime that significantly
reduces possibility of mixing its arguments by an accident.

import Data.Time.Clock.NonNegativeTimeDiff
f = do
  started <- getCurrentTime
  threadDelay 10_000_000
  ended <- getTimeAfter started
  pure $ ended `diffUTCTime` started

STM use case

The STM package is shipped without a function to get current time.
Let’s consider a situtation like this:

data Ctx
  = Ctx { m :: Map Int UTCTime
        , s :: TVar NominalDiffTime
        , q :: TQueue Int
        }

f (c :: Ctx) = do
  now <- getCurrentTime
  atomically $ do
    i <- readTQueue q
    lookup i c.m >>= \case
      Nothing -> pure ()
      Just t -> modifyTVar' c.s (+ diffUTCTime now t)

now might be less than t because the queue might be empty by the time f is invoked. The package API enforces the correct way:

data Ctx
  = Ctx { m :: Map Int UtcBox
        , s :: TVar NominalDiffTime
        , q :: TQueue Int
        }

f (c :: Ctx) = do
  atomically $ do
    i <- readTQueue q
    lookup i c.m >>= \case
      Nothing -> pure ()
      Just t ->
        doAfter tb \t -> do
          now <- getTimeAfter t
          modifyTVar' c.s (+ diffUTCTime now t)

File access time

Another popular use case where original diffUTCTime might be misused.

isFileOlderThan :: FilePath -> NominalDiffTime -> IO Bool
isFileOlderThan fp maxAge = do
  now <- getCurrentTime
  mt <- getModificationTime fp
  when (mt `diffUTCTime` now > maxAge) $ do
    removeFile fp

File age is always negative in the above example - this eventually
would cause a space leak on disk.

Corrected version:

isFileOlderThan :: FilePath -> NominalDiffTime -> IO Bool
isFileOlderThan fp maxAge =
  getModificationTime fp >>= (`doAfter` \mt -> do
    now <- getTimeAfter mt
    when (now `diffUTCTime` mt > maxAge) $ do
      removeFile fp)
1 Like

So… do you believe this is a better solution than testing the application and, perhaps, logging the delta time?


Have you read the documentation to unsafeIOToSTM?

Not clear when this situation might arise, please elaborate.

I feel like adding a way to get time inside of STM is kind of antithetical to what STM wants; STM wants to be able to freely (with minimal side effects) retry the entire block, but now there’s some outside function leaking in. I’m sure someone more knowledgeable will comment otherwise but that sticks out to me.

I do enjoy the use of the type level nats to ensure one after the other, and a secondary library like this is the correct place to do that.

2 Likes

Yes, I do. getCurrentTime doesn’t modify anything.
Otherwise I need to break STM transaction to get time.
GitHub has bug tracker.