• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

haskell / random / 433

05 Jul 2025 08:38PM UTC coverage: 69.035% (+0.3%) from 68.696%
433

push

github

web-flow
Merge e34e106b0 into 1592c8382

141 of 209 new or added lines in 6 files covered. (67.46%)

98 existing lines in 6 files now uncovered.

651 of 943 relevant lines covered (69.03%)

1.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

47.5
/src/System/Random/Seed.hs
1
{-# LANGUAGE AllowAmbiguousTypes #-}
2
{-# LANGUAGE BangPatterns #-}
3
{-# LANGUAGE DataKinds #-}
4
{-# LANGUAGE FlexibleContexts #-}
5
{-# LANGUAGE LambdaCase #-}
6
{-# LANGUAGE MagicHash #-}
7
{-# LANGUAGE ScopedTypeVariables #-}
8
{-# LANGUAGE Trustworthy #-}
9
{-# LANGUAGE TypeApplications #-}
10
{-# LANGUAGE TypeFamilies #-}
11
{-# LANGUAGE TypeOperators #-}
12
{-# LANGUAGE UndecidableInstances #-}
13
{-# LANGUAGE UndecidableSuperClasses #-}
14
{-# OPTIONS_GHC -Wno-orphans #-}
15

16
-- |
17
-- Module      :  System.Random.Seed
18
-- Copyright   :  (c) Alexey Kuleshevich 2024
19
-- License     :  BSD-style (see the file LICENSE in the 'random' repository)
20
-- Maintainer  :  libraries@haskell.org
21
module System.Random.Seed (
22
  SeedGen (..),
23

24
  -- ** Seed
25
  Seed,
26
  seedSize,
27
  seedSizeProxy,
28
  mkSeed,
29
  unSeed,
30
  mkSeedFromByteString,
31
  unSeedToByteString,
32
  withSeed,
33
  withSeedM,
34
  withSeedFile,
35
  seedGenTypeName,
36
  nonEmptyToSeed,
37
  nonEmptyFromSeed,
38
) where
39

40
import Control.Monad (unless)
41
import qualified Control.Monad.Fail as F
42
import Control.Monad.IO.Class
43
import Control.Monad.ST
44
import Control.Monad.State.Strict (get, put, runStateT)
45
import Data.Array.Byte (ByteArray (..))
46
import Data.Bits
47
import qualified Data.ByteString as BS
48
import qualified Data.ByteString.Short.Internal as SBS (fromShort, toShort)
49
import Data.Coerce
50
import Data.Functor.Identity (runIdentity)
51
import Data.List.NonEmpty as NE (NonEmpty (..), nonEmpty, toList)
52
import Data.Typeable
53
import Data.Word
54
import GHC.Exts (Proxy#, proxy#)
55
import GHC.TypeLits (KnownNat, Nat, natVal', type (<=))
56
import System.Random.Internal
57
import qualified System.Random.SplitMix as SM
58
import qualified System.Random.SplitMix32 as SM32
59

60
-- | Interface for converting a pure pseudo-random number generator to and from non-empty
61
-- sequence of bytes. Seeds are stored in Little-Endian order regardless of the platform
62
-- it is being used on, which provides cross-platform compatibility, while providing
63
-- optimal performance for the most common platform type.
64
--
65
-- Conversion to and from a `Seed` serves as a building block for implementing
66
-- serialization for any pure or frozen pseudo-random number generator.
67
--
68
-- It is not trivial to implement platform independence. For this reason this type class
69
-- has two alternative ways of creating an instance for this class. The easiest way for
70
-- constructing a platform indepent seed is by converting the inner state of a generator
71
-- to and from a list of 64 bit words using `toSeed64` and `fromSeed64` respectively. In
72
-- that case cross-platform support will be handled automaticaly.
73
--
74
-- >>> :set -XDataKinds -XTypeFamilies
75
-- >>> import Data.Word (Word8, Word32)
76
-- >>> import Data.Bits ((.|.), shiftR, shiftL)
77
-- >>> import Data.List.NonEmpty (NonEmpty ((:|)))
78
-- >>> data FiveByteGen = FiveByteGen Word8 Word32 deriving Show
79
-- >>> :{
80
-- instance SeedGen FiveByteGen where
81
--   type SeedSize FiveByteGen = 5
82
--   fromSeed64 (w64 :| _) =
83
--     FiveByteGen (fromIntegral (w64 `shiftR` 32)) (fromIntegral w64)
84
--   toSeed64 (FiveByteGen x1 x4) =
85
--     let w64 = (fromIntegral x1 `shiftL` 32) .|. fromIntegral x4
86
--      in (w64 :| [])
87
-- :}
88
--
89
-- >>> FiveByteGen 0x80 0x01020304
90
-- FiveByteGen 128 16909060
91
-- >>> fromSeed (toSeed (FiveByteGen 0x80 0x01020304))
92
-- FiveByteGen 128 16909060
93
-- >>> toSeed (FiveByteGen 0x80 0x01020304)
94
-- Seed [0x04, 0x03, 0x02, 0x01, 0x80]
95
-- >>> toSeed64 (FiveByteGen 0x80 0x01020304)
96
-- 549772722948 :| []
97
--
98
-- However, when performance is of utmost importance or default handling of cross platform
99
-- independence is not sufficient, then an adventurous developer can try implementing
100
-- conversion into bytes directly with `toSeed` and `fromSeed`.
101
--
102
-- Properties that must hold:
103
--
104
-- @
105
-- > fromSeed (toSeed gen) == gen
106
-- @
107
--
108
-- @
109
-- > fromSeed64 (toSeed64 gen) == gen
110
-- @
111
--
112
-- Note, that there is no requirement for every `Seed` to roundtrip, eg. this proprty does
113
-- not even hold for `StdGen`:
114
--
115
-- >>> let seed = nonEmptyToSeed (0xab :| [0xff00]) :: Seed StdGen
116
-- >>> seed == toSeed (fromSeed seed)
117
-- False
118
--
119
-- @since 1.3.0
120
class (KnownNat (SeedSize g), 1 <= SeedSize g, Typeable g) => SeedGen g where
121
  -- | Number of bytes that is required for storing the full state of a pseudo-random
122
  -- number generator. It should be big enough to satisfy the roundtrip property:
123
  --
124
  -- @
125
  -- > fromSeed (toSeed gen) == gen
126
  -- @
127
  type SeedSize g :: Nat
128

129
  {-# MINIMAL (fromSeed, toSeed) | (fromSeed64, toSeed64) #-}
130

131
  -- | Convert from a binary representation to a pseudo-random number generator
132
  --
133
  -- @since 1.3.0
134
  fromSeed :: Seed g -> g
135
  fromSeed = fromSeed64 . nonEmptyFromSeed
2✔
136

137
  -- | Convert to a binary representation of a pseudo-random number generator
138
  --
139
  -- @since 1.3.0
140
  toSeed :: g -> Seed g
141
  toSeed = nonEmptyToSeed . toSeed64
2✔
142

143
  -- | Construct pseudo-random number generator from a list of words. Whenever list does
144
  -- not have enough bytes to satisfy the `SeedSize` requirement, it will be padded with
145
  -- zeros. On the other hand when it has more than necessary, extra bytes will be dropped.
146
  --
147
  -- For example if `SeedSize` is set to 2, then only the lower 16 bits of the first
148
  -- element in the list will be used.
149
  --
150
  -- @since 1.3.0
151
  fromSeed64 :: NonEmpty Word64 -> g
152
  fromSeed64 = fromSeed . nonEmptyToSeed
2✔
153

154
  -- | Convert pseudo-random number generator to a list of words
155
  --
156
  -- In case when `SeedSize` is not a multiple of 8, then the upper bits of the last word
157
  -- in the list will be set to zero.
158
  --
159
  -- @since 1.3.0
160
  toSeed64 :: g -> NonEmpty Word64
161
  toSeed64 = nonEmptyFromSeed . toSeed
2✔
162

163
instance SeedGen StdGen where
164
  type SeedSize StdGen = SeedSize SM.SMGen
165
  fromSeed = coerce (fromSeed :: Seed SM.SMGen -> SM.SMGen)
2✔
166
  toSeed = coerce (toSeed :: SM.SMGen -> Seed SM.SMGen)
2✔
167

168
instance SeedGen g => SeedGen (StateGen g) where
169
  type SeedSize (StateGen g) = SeedSize g
UNCOV
170
  fromSeed = coerce (fromSeed :: Seed g -> g)
×
171
  toSeed = coerce (toSeed :: g -> Seed g)
×
172

173
instance SeedGen SM.SMGen where
174
  type SeedSize SM.SMGen = 16
175
  fromSeed (Seed ba) =
2✔
176
    SM.seedSMGen (indexWord64LE ba 0) (indexWord64LE ba 8)
2✔
177
  toSeed g =
2✔
178
    case SM.unseedSMGen g of
2✔
179
      (seed, gamma) -> Seed $ runST $ do
2✔
180
        mba <- newMutableByteArray 16
2✔
181
        writeWord64LE mba 0 seed
2✔
182
        writeWord64LE mba 8 gamma
2✔
183
        freezeMutableByteArray mba
2✔
184

185
instance SeedGen SM32.SMGen where
186
  type SeedSize SM32.SMGen = 8
UNCOV
187
  fromSeed (Seed ba) =
×
188
    let x = indexWord64LE ba 0
×
189
        seed, gamma :: Word32
UNCOV
190
        seed = fromIntegral (shiftR x 32)
×
191
        gamma = fromIntegral x
×
NEW
192
     in SM32.seedSMGen seed gamma
×
193
  toSeed g =
×
194
    let seed, gamma :: Word32
×
195
        (seed, gamma) = SM32.unseedSMGen g
×
NEW
196
     in Seed $ runST $ do
×
NEW
197
          mba <- newMutableByteArray 8
×
198
          let w64 :: Word64
NEW
199
              w64 = shiftL (fromIntegral seed) 32 .|. fromIntegral gamma
×
NEW
200
          writeWord64LE mba 0 w64
×
NEW
201
          freezeMutableByteArray mba
×
202

203
instance SeedGen g => Uniform (Seed g) where
UNCOV
204
  uniformM = fmap Seed . uniformByteArrayM False (seedSize @g)
×
205

206
-- | Get the expected size of the `Seed` in number bytes
207
--
208
-- @since 1.3.0
209
seedSize :: forall g. SeedGen g => Int
210
seedSize = fromInteger $ natVal' (proxy# :: Proxy# (SeedSize g))
2✔
211

212
-- | Just like `seedSize`, except it accepts a proxy as an argument.
213
--
214
-- @since 1.3.0
215
seedSizeProxy :: forall proxy g. SeedGen g => proxy g -> Int
UNCOV
216
seedSizeProxy _px = seedSize @g
×
217

218
-- | Construct a `Seed` from a `ByteArray` of expected length. Whenever `ByteArray` does
219
-- not match the `SeedSize` specified by the pseudo-random generator, this function will
220
-- `F.fail`.
221
--
222
-- @since 1.3.0
223
mkSeed :: forall g m. (SeedGen g, F.MonadFail m) => ByteArray -> m (Seed g)
224
mkSeed ba = do
2✔
225
  unless (sizeOfByteArray ba == seedSize @g) $ do
1✔
NEW
226
    F.fail $
×
NEW
227
      "Unexpected number of bytes: "
×
228
        ++ show (sizeOfByteArray ba)
×
229
        ++ ". Exactly "
×
230
        ++ show (seedSize @g)
×
231
        ++ " bytes is required by the "
×
232
        ++ show (seedGenTypeName @g)
×
233
  pure $ Seed ba
2✔
234

235
-- | Helper function that allows for operating directly on the `Seed`, while supplying a
236
-- function that uses the pseudo-random number generator that is constructed from that
237
-- `Seed`.
238
--
239
-- ====__Example__
240
--
241
-- >>> :set -XTypeApplications
242
-- >>> import System.Random
243
-- >>> withSeed (nonEmptyToSeed (pure 2024) :: Seed StdGen) (uniform @Int)
244
-- (1039666877624726199,Seed [0xe9, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
245
--
246
-- @since 1.3.0
247
withSeed :: SeedGen g => Seed g -> (g -> (a, g)) -> (a, Seed g)
UNCOV
248
withSeed seed f = runIdentity (withSeedM seed (pure . f))
×
249

250
-- | Same as `withSeed`, except it is useful with monadic computation and frozen generators.
251
--
252
-- See `System.Random.Stateful.withSeedMutableGen` for a helper that also handles seeds
253
-- for mutable pseduo-random number generators.
254
--
255
-- @since 1.3.0
256
withSeedM :: (SeedGen g, Functor f) => Seed g -> (g -> f (a, g)) -> f (a, Seed g)
UNCOV
257
withSeedM seed f = fmap toSeed <$> f (fromSeed seed)
×
258

259
-- | This is a function that shows the name of the generator type, which is useful for
260
-- error reporting.
261
--
262
-- @since 1.3.0
263
seedGenTypeName :: forall g. SeedGen g => String
264
seedGenTypeName = show (typeOf (Proxy @g))
1✔
265

266
-- | Just like `mkSeed`, but uses `ByteString` as argument. Results in a memcopy of the seed.
267
--
268
-- @since 1.3.0
269
mkSeedFromByteString :: (SeedGen g, F.MonadFail m) => BS.ByteString -> m (Seed g)
UNCOV
270
mkSeedFromByteString = mkSeed . shortByteStringToByteArray . SBS.toShort
×
271

272
-- | Unwrap the `Seed` and get the underlying `ByteArray`
273
--
274
-- @since 1.3.0
275
unSeed :: Seed g -> ByteArray
276
unSeed (Seed ba) = ba
2✔
277

278
-- | Just like `unSeed`, but produced a `ByteString`. Results in a memcopy of the seed.
279
--
280
-- @since 1.3.0
281
unSeedToByteString :: Seed g -> BS.ByteString
UNCOV
282
unSeedToByteString = SBS.fromShort . byteArrayToShortByteString . unSeed
×
283

284
-- | Read the seed from a file and use it for constructing a pseudo-random number
285
-- generator. After supplied action has been applied to the constructed generator, the
286
-- resulting generator will be converted back to a seed and written to the same file.
287
--
288
-- @since 1.3.0
289
withSeedFile :: (SeedGen g, MonadIO m) => FilePath -> (Seed g -> m (a, Seed g)) -> m a
UNCOV
290
withSeedFile fileName action = do
×
291
  bs <- liftIO $ BS.readFile fileName
×
292
  seed <- liftIO $ mkSeedFromByteString bs
×
293
  (res, seed') <- action seed
×
294
  liftIO $ BS.writeFile fileName $ unSeedToByteString seed'
×
295
  pure res
×
296

297
-- | Construct a seed from a list of 64-bit words. At most `SeedSize` many bytes will be used.
298
--
299
-- @since 1.3.0
300
nonEmptyToSeed :: forall g. SeedGen g => NonEmpty Word64 -> Seed g
301
nonEmptyToSeed xs = Seed $ runST $ do
2✔
302
  let n = seedSize @g
2✔
303
  mba <- newMutableByteArray n
2✔
304
  _ <- flip runStateT (NE.toList xs) $ do
2✔
305
    defaultUnsafeFillMutableByteArrayT mba 0 n $ do
2✔
306
      get >>= \case
2✔
UNCOV
307
        [] -> pure 0
×
308
        w : ws -> w <$ put ws
2✔
309
  freezeMutableByteArray mba
2✔
310

311
-- | Convert a `Seed` to a list of 64bit words.
312
--
313
-- @since 1.3.0
314
nonEmptyFromSeed :: forall g. SeedGen g => Seed g -> NonEmpty Word64
315
nonEmptyFromSeed (Seed ba) =
2✔
316
  case nonEmpty $ reverse $ goWord64 0 [] of
2✔
317
    Just ne -> ne
2✔
318
    Nothing ->
319
      -- Seed is at least 1 byte in size, so it can't be empty
NEW
320
      error $
×
NEW
321
        "Impossible: Seed for "
×
NEW
322
          ++ seedGenTypeName @g
×
NEW
323
          ++ " must be at least: "
×
NEW
324
          ++ show (seedSize @g)
×
NEW
325
          ++ " bytes, but got "
×
NEW
326
          ++ show n
×
327
  where
328
    n = sizeOfByteArray ba
2✔
329
    n8 = 8 * (n `quot` 8)
2✔
330
    goWord64 i !acc
2✔
331
      | i < n8 = goWord64 (i + 8) (indexWord64LE ba i : acc)
2✔
332
      | i == n = acc
2✔
333
      | otherwise = indexByteSliceWord64LE ba i n : acc
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc