5
votes

Using QuickCheck, I'd like to create a series of pseudorandom TimeOfDay values.

It's easy to create a specific TimeOfDay:

now = TimeOfDay 17 35 22

Printing this with GHCi 8.6.5 yields:

17:35:22

I thought that the Arbitrary instance necessary for creating TimeOfDay values with QuickCheck would thus be:

instance Arbitrary TimeOfDay where
  arbitrary = do
    hour <- elements [0 .. 23]
    min  <- elements [0 .. 59]
    -- Going till 60 accounts for leap seconds
    sec  <- elements [0 .. 60]
    return $ TimeOfDay hour min sec

Although this typechecks, running the following line hangs GHCi and after a couple of seconds writes Killed to the console:

sample (arbitrary :: Gen TimeOfDay)

Where's the bug?

2

2 Answers

6
votes

As you found out, the todSeconds has as type Pico which is a fixed-point number with a resolution of 10-12, so that means that [0 .. 60] has 6×1013+1 values. This will easily take ~1000 seconds to iterate over the entire list.

That being said, you do not need to use elements here in the first place. We can use choose :: Random a => (a, a) -> Gen a that will generate a random value within bounds (both bounds inclusive).

We can then define our Arbitrary as:

instance Arbitrary TimeOfDay where
    arbitrary = TimeOfDay
        <$> choose (0, 23)
        <*> choose (0, 59)
        <*> (fmap MkFixed (choose (0, 61*10^12-1)))

This then gives us:

Main> sample (arbitrary :: Gen TimeOfDay)
15:45:04.132804129488
11:06:12.447614162981
12:07:50.773642440667
04:40:47.966398431784
02:30:09.60931551059
00:51:46.564756092467
07:57:44.170698241052
02:45:57.743854623407
00:17:22.627238967351
13:03:57.364852826473
11:12:34.894890974241

If you do not want these picoseconds, we can do the multiplication in the fmap:

instance Arbitrary TimeOfDay where
    arbitrary = TimeOfDay
        <$> choose (0, 23)
        <*> choose (0, 59)
        <*> (fmap (MkFixed . (10^12 *)) (choose (0, 60)))

Then we obtain:

Main> sample (arbitrary :: Gen TimeOfDay)
15:00:53
14:02:44
14:44:40
12:40:12
09:55:39
10:06:02
15:00:51
15:52:23
16:59:05
22:38:45
20:23:15
2
votes

The reason for the bug is that TimeOfDay's last constructor parameter is the number of seconds with picosecond resolution.

I assume that consequently, sec <- elements [0 .. 60] wasn't generating one of 61 values but one of 61 * 10¹² values.

This fixes the issue:

instance Arbitrary TimeOfDay where
  arbitrary = do
    hour <- elements [0 .. 23]
    min  <- elements [0 .. 59]
    -- Going till 60 accounts for leap seconds
    picoSecond <- elements [1, 2 .. 60]
    return $ TimeOfDay hour min picoSecond

Now, sample (arbitrary :: Gen TimeOfDay) produces for example:

17:19:03
15:49:58
01:28:40
13:20:07
12:00:01
12:35:45
13:25:33
12:55:20
07:11:54
21:10:46
14:34:15

If you also want values in the subsecond range, change the step width of the list generator:

picoSecond <- elements [1, 1.1 .. 60]

Sample values:

04:09:23.6
10:22:50.4
18:56:57.6
07:12:07.8
08:12:02.6
14:20:40
05:42:21.4
14:35:42.3
02:47:32.6
22:02:26.2
08:26:09.4