An experiment with typed time 2014-05-24

 

haskell, closed type families, time, units

tl;dr: Using the units package for correctly specifying delays

Motivation

So, I was doing some experiences with the Retry module, that accepts a base delay as an Int in milliseconds. Then, I was testing this with threadDelay which accepts microseconds.

But the code I was working on actually required delays in the order of seconds, and perhaps even minutes.

As you can now probably guess, this is asking for trouble.

Naive solution

Well, I could just define functions like these:

-- Convert seconds to milliseconds
secondsToMs :: Int -> Int
secondsToMs = (*1000)

-- Convert seconds do microseconds
secondsToUs :: Int -> Int
secondsToUs = (*1000000)

But this is still asking for trouble. Nothing is stopping me from mixing seconds with milliseconds with microseconds. Besides, having toUs just feels wrong :P

Libraries ahoy

Looking in hackage for more type safe solutions yields a lot of libraries. I’ll focus first in two time specific libraries:

tiempo

Tiempo, found here seems to be just a proof-of-concept on expanding the implementation I presented above.

You do get a specific type, TimeInterval, and functions to convert to and from.

However, its interface is far from uniform:

toMicroSeconds :: TimeInterval -> Int
toMilliSeconds :: TimeInterval -> Double
toSeconds      :: TimeInterval -> Double

While I can see why they did this - some base functions dealing with delays use Int as microseconds -, it doesn’t feel clean.

Also, there is no Num instance, thus you can’t even add or subtract TimeIntervals.

However, there is one thing I like in this library: it redefines the threadDelay and timeout functions to accept a TimeInterval, which is one of my goals.

time-units

This library, found here, seems to get some things right. A Num instance makes it easy to declare values:

import Data.Time.Units

let x = 5 :: Second
print x
> 5s

And there are separate data types for seconds, minutes, etc, so you can’t accidentally mix them up. You can use the convertUnit to perform (sometimes lossy) conversions.

But, this does imply a somewhat bulky way of adding up times:

import Data.Time.Units

let x = convertUnit ((5 :: Millisecond) + convertUnit (6 :: Second)) :: Microsecond
> 6005000µs

General unit libraries

Next, I finally stumbled on www.haskell.org/haskellwiki/Physical_units. There are lots of libraries for achieving this, and to be honest, I was a little bit lost.

And if you are hopping for a detailed technical explanation regarding my following choice, I am afraid I don’t have one. I just choose one which seemed to be actively developed, and was mainly focused on unit support. And, of course, that was usable by a dumb guy like me.

Thus, I ended up choosing units over unittyped mainly because the later only has one release in Hackage.

Units

This library is actually split in two:

  • units : implements all the Units type hackery. Its type signatures do seem a little daunting, but if you stick with the functions I’ll describe next, you should not have a problem.
  • units-defs : The above library does not actually define any Unit besides the very generic Scalar. But you can import units-defs to have access to most (all?) units you might want.

Minimum viable set of functions

So, you want to use time. Fine:

import Data.Metrology
import Data.Metrology.SI
import Data.Metrology.Show

let x = 5 %% Second
let y = 6 %% milli Second
let z = x |+| y 
z
> 5.006 s

Looks great! What if I want the results in microseconds?

let ms = z ## micro Second
> 5006000.0

Even better! As you can see, with this solution you can freely mix different values of the same physical quantity, and you always get a correct result. And you can easily convert the final result to the format of your preference.

While there is no Num instance, the |+|, |*|, etc operators do everything you might need.

Thus, while the type signatures do seem a little daunting, the actual use is quite easy!

A notorious disadvantage of this solution is that it uses closed type families, and thus it only supports GHC 7.8.2+. But if you are not bound to a specific GHC version, this is not a major limitation.

Implementing threadDelay

So, can we implement a threadDelay which accepts Time as the delay unit?

Yes, let me show you how:

{-# LANGUAGE TypeFamilies  #-}
module Control.Concurrent.Units
  ( threadDelay
  , milli
  , micro
  ) where

import qualified Control.Concurrent as Conc
import Data.Metrology
import Data.Metrology.SI

-- | Modified thread delay that accepts Time.
threadDelay :: Time -> IO ()
threadDelay t = Conc.threadDelay (truncate(t ## micro Second))

So, this is really quite simple, ## micro Second converts the result into a (Fractional) representation, and then round converts it to an Int acceptable by the old threadDelay (note that round may lose precision).

And what about minutes and hours

Well, the SI does not define minutes and hours, only seconds. And lets face it, nobody uses kilo seconds.

So, lets add them to our little module:

data Minute = Minute
instance Unit Minute where
  type BaseUnit Minute = Second
  conversionRatio _ = 60
instance Show Minute where
  show _ = "min"

data Hour = Hour
instance Unit Hour where
  type BaseUnit Hour = Second
  conversionRatio _ = 60 * 60
instance Show Hour where
  show _ = "hour"

This is just code copied from units-defs Originally I defined Minute and Hour as Prefixes, but 5 %% hour Second just didn’t feel right. With the above implementation, I can just type:

threadDelay (2.5 %% Minute)

Is this a good solution?

Well… It is a bit overkill, to be honest. And don’t get illusions of precision when dealing with time, and threadDelay: this is not a RTOS. Please note that this has nothing to do with the units package precision, but the actual use of precise time in a non-real-time operating system as Linux, Windows or OSX.

What this solution does allow you is to think naturally in whatever units you prefer, mix them, and have the compiler do the dirty work for you.

And for me, that’s a good enough advantage!

Addendum

  • PS1: After talking with Richard Eisenberg, one of the library authors, he seems open to exchanging the names of the more polymorphic # and % with the monomorphic ## and %%. If you have no idea what this means, it is simple: the monomorphic will guess your type much easier, without explicit type annotations. But in the presence of proper type annotations, they should behave the same.

  • PS2: If your interested in this kind of mathematical libraries, two other very popular libraries seems to be the numeric-prelude and algebra, but these are much more general libraries…

  • PS3: User dmwit on Reddit correctly points out a limitation with Control.Concurrent.threadDelay accepting the delay as an Int representing us: it is very easy to overflow, yielding unpredictable results. The unbounded-delays solves this problem, and I’ve implemented the approach presented in this article as a new library built on top of this one. I hope you might find it useful.

comments powered by Disqus