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

haskell / random / 436

06 Jul 2025 08:53PM UTC coverage: 69.108% (+0.07%) from 69.035%
436

push

github

web-flow
Merge 724bc6e23 into ca8a869de

27 of 30 new or added lines in 3 files covered. (90.0%)

7 existing lines in 1 file now uncovered.

651 of 942 relevant lines covered (69.11%)

1.3 hits per line

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

46.34
/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
-- Standalone definitions due to GHC-8.0 not supporting deriving with associated type families
169

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

175
instance SeedGen g => SeedGen (AtomicGen g) where
176
  type SeedSize (AtomicGen g) = SeedSize g
NEW
177
  fromSeed = coerce (fromSeed :: Seed g -> g)
×
NEW
178
  toSeed = coerce (toSeed :: g -> Seed g)
×
179

180
instance SeedGen SM.SMGen where
181
  type SeedSize SM.SMGen = 16
182
  fromSeed (Seed ba) =
2✔
183
    SM.seedSMGen (indexWord64LE ba 0) (indexWord64LE ba 8)
2✔
184
  toSeed g =
2✔
185
    case SM.unseedSMGen g of
2✔
186
      (seed, gamma) -> Seed $ runST $ do
2✔
187
        mba <- newMutableByteArray 16
2✔
188
        writeWord64LE mba 0 seed
2✔
189
        writeWord64LE mba 8 gamma
2✔
190
        freezeMutableByteArray mba
2✔
191

192
instance SeedGen SM32.SMGen where
193
  type SeedSize SM32.SMGen = 8
194
  fromSeed (Seed ba) =
×
195
    let x = indexWord64LE ba 0
×
196
        seed, gamma :: Word32
197
        seed = fromIntegral (shiftR x 32)
×
198
        gamma = fromIntegral x
×
199
     in SM32.seedSMGen seed gamma
×
200
  toSeed g =
×
201
    let seed, gamma :: Word32
×
202
        (seed, gamma) = SM32.unseedSMGen g
×
203
     in Seed $ runST $ do
×
204
          mba <- newMutableByteArray 8
×
205
          let w64 :: Word64
206
              w64 = shiftL (fromIntegral seed) 32 .|. fromIntegral gamma
×
207
          writeWord64LE mba 0 w64
×
208
          freezeMutableByteArray mba
×
209

210
instance SeedGen g => Uniform (Seed g) where
211
  uniformM = fmap Seed . uniformByteArrayM False (seedSize @g)
×
212

213
-- | Get the expected size of the `Seed` in number bytes
214
--
215
-- @since 1.3.0
216
seedSize :: forall g. SeedGen g => Int
217
seedSize = fromInteger $ natVal' (proxy# :: Proxy# (SeedSize g))
2✔
218

219
-- | Just like `seedSize`, except it accepts a proxy as an argument.
220
--
221
-- @since 1.3.0
222
seedSizeProxy :: forall proxy g. SeedGen g => proxy g -> Int
223
seedSizeProxy _px = seedSize @g
×
224

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

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

257
-- | Same as `withSeed`, except it is useful with monadic computation and frozen generators.
258
--
259
-- See `System.Random.Stateful.withSeedMutableGen` for a helper that also handles seeds
260
-- for mutable pseduo-random number generators.
261
--
262
-- @since 1.3.0
263
withSeedM :: (SeedGen g, Functor f) => Seed g -> (g -> f (a, g)) -> f (a, Seed g)
264
withSeedM seed f = fmap toSeed <$> f (fromSeed seed)
×
265

266
-- | This is a function that shows the name of the generator type, which is useful for
267
-- error reporting.
268
--
269
-- @since 1.3.0
270
seedGenTypeName :: forall g. SeedGen g => String
271
seedGenTypeName = show (typeOf (Proxy @g))
1✔
272

273
-- | Just like `mkSeed`, but uses `ByteString` as argument. Results in a memcopy of the seed.
274
--
275
-- @since 1.3.0
276
mkSeedFromByteString :: (SeedGen g, F.MonadFail m) => BS.ByteString -> m (Seed g)
277
mkSeedFromByteString = mkSeed . shortByteStringToByteArray . SBS.toShort
×
278

279
-- | Unwrap the `Seed` and get the underlying `ByteArray`
280
--
281
-- @since 1.3.0
282
unSeed :: Seed g -> ByteArray
283
unSeed (Seed ba) = ba
2✔
284

285
-- | Just like `unSeed`, but produced a `ByteString`. Results in a memcopy of the seed.
286
--
287
-- @since 1.3.0
288
unSeedToByteString :: Seed g -> BS.ByteString
289
unSeedToByteString = SBS.fromShort . byteArrayToShortByteString . unSeed
×
290

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

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

318
-- | Convert a `Seed` to a list of 64bit words.
319
--
320
-- @since 1.3.0
321
nonEmptyFromSeed :: forall g. SeedGen g => Seed g -> NonEmpty Word64
322
nonEmptyFromSeed (Seed ba) =
2✔
323
  case nonEmpty $ reverse $ goWord64 0 [] of
2✔
324
    Just ne -> ne
2✔
325
    Nothing ->
326
      -- Seed is at least 1 byte in size, so it can't be empty
327
      error $
×
328
        "Impossible: Seed for "
×
329
          ++ seedGenTypeName @g
×
330
          ++ " must be at least: "
×
331
          ++ show (seedSize @g)
×
332
          ++ " bytes, but got "
×
333
          ++ show n
×
334
  where
335
    n = sizeOfByteArray ba
2✔
336
    n8 = 8 * (n `quot` 8)
2✔
337
    goWord64 i !acc
2✔
338
      | i < n8 = goWord64 (i + 8) (indexWord64LE ba i : acc)
2✔
339
      | i == n = acc
2✔
340
      | 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