Skip to content

Commit c8657ea

Browse files
committed
Fix up frequency
* Make `frequency` detect negative frequencies and frequency total overflow. * Rewrite `frequency` to build an `IntMap` to represent the frequency list, which greatly improves efficiency for long lists. Fixes #337.
1 parent 420bd95 commit c8657ea

File tree

1 file changed

+33
-19
lines changed
  • hedgehog/src/Hedgehog/Internal

1 file changed

+33
-19
lines changed

hedgehog/src/Hedgehog/Internal/Gen.hs

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{-# OPTIONS_HADDOCK not-home #-}
22
{-# LANGUAGE ApplicativeDo #-}
3+
{-# LANGUAGE BangPatterns #-}
34
{-# LANGUAGE CPP #-}
45
{-# LANGUAGE DataKinds #-}
56
{-# LANGUAGE DeriveFoldable #-}
@@ -12,6 +13,7 @@
1213
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
1314
{-# LANGUAGE LambdaCase #-}
1415
{-# LANGUAGE MultiParamTypeClasses #-}
16+
{-# LANGUAGE PatternGuards #-}
1517
{-# LANGUAGE RankNTypes #-}
1618
{-# LANGUAGE ScopedTypeVariables #-}
1719
{-# LANGUAGE StandaloneDeriving #-}
@@ -202,6 +204,8 @@ import Data.Coerce (coerce)
202204
import Data.Foldable (for_, toList)
203205
import Data.Functor.Identity (Identity(..))
204206
import Data.Int (Int8, Int16, Int32, Int64)
207+
import qualified Data.IntMap.Strict as IM
208+
import qualified Data.List as List
205209
import Data.List.NonEmpty (NonEmpty)
206210
import qualified Data.List.NonEmpty as NonEmpty
207211
import Data.Map (Map)
@@ -1170,28 +1174,38 @@ choice = \case
11701174
--
11711175
-- This generator shrinks towards the first generator in the list.
11721176
--
1173-
-- /The input list must be non-empty./
1177+
-- /The sum of the frequencies must be at least @1@ and at most @'maxBound' :: 'Int'@.
1178+
-- No frequency may be negative./
11741179
--
11751180
frequency :: MonadGen m => [(Int, m a)] -> m a
1176-
frequency = \case
1177-
[] ->
1178-
error "Hedgehog.Gen.frequency: used with empty list"
1179-
xs0 -> do
1180-
let
1181-
pick n = \case
1182-
[] ->
1183-
error "Hedgehog.Gen.frequency/pick: used with empty list"
1184-
(k, x) : xs ->
1185-
if n <= k then
1186-
x
1187-
else
1188-
pick (n - k) xs
1189-
1190-
total =
1191-
sum (fmap fst xs0)
1192-
1181+
-- We calculate a running sum of the individual frequencies and build
1182+
-- an IntMap mapping the results to the generators. This makes the
1183+
-- resulting generator much faster than a naive list-based one when
1184+
-- the input list is long, and not much slower when it's short.
1185+
frequency xs0 =
1186+
do
11931187
n <- integral $ Range.constant 1 total
1194-
pick n xs0
1188+
case IM.lookupGE n sum_map of
1189+
Just (_, a) -> a
1190+
Nothing -> error "Hedgehog.Gen.frequency: Something went wrong."
1191+
where
1192+
--[(1, x), (7, y), (10, z)] In
1193+
--[(1, x), (8, y), (18, z)] Out
1194+
sum_map = IM.fromDistinctAscList $ List.unfoldr go (0, xs0)
1195+
where
1196+
go (_, []) = Nothing
1197+
go (n, (k, x) : xs)
1198+
| k < 0 = error "Hedgehog.Gen.frequency: Negative frequency."
1199+
-- nk < 0 means the sum overflowed.
1200+
| nk < 0 = error "Hedgehog.Gen.frequency: Frequency sum above maxBound :: Int"
1201+
| k > 0 = Just ((nk, x), (nk, xs))
1202+
| otherwise = go (n, xs)
1203+
where !nk = n + fromIntegral k
1204+
total
1205+
| Just (mx, _) <- IM.lookupMax sum_map
1206+
= mx
1207+
| otherwise
1208+
= error "Hedgehog.Gen.frequency: frequencies sum to zero"
11951209

11961210
-- | Modifies combinators which choose from a list of generators, like 'choice'
11971211
-- or 'frequency', so that they can be used in recursive scenarios.

0 commit comments

Comments
 (0)