grapesy-1.0.0: kvstore/KVStore/Util/RandomGen.hs
{-# OPTIONS_GHC -O2 #-}
-- | Stateful random number generation
--
-- Intended for qualified import.
--
-- > import KVStore.Util.RandomGen (RandomGen)
-- > import KVStore.Util.RandomGen qualified as RandomGen
module KVStore.Util.RandomGen (
RandomGen -- opaque
-- * API
, new
, nextDouble
, nextWord64
, nextInt
, RandomBytes(..)
, nextBytes
) where
import Control.Concurrent
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Base16 qualified as BS.Base16
import Data.ByteString.Char8 qualified as BS.Char8
import Data.ByteString.Unsafe (unsafePackMallocCStringLen)
import Data.Tuple (swap)
import Data.Word
import Foreign qualified as C
import Foreign.Ptr
import System.Random.SplitMix (SMGen)
import System.Random.SplitMix qualified as SplitMix
{-------------------------------------------------------------------------------
Definition
-------------------------------------------------------------------------------}
-- | Thread-safe PRNG
newtype RandomGen = Wrap {
unwrap :: MVar SMGen
}
{-------------------------------------------------------------------------------
Basic API
-------------------------------------------------------------------------------}
new :: IO RandomGen
new = SplitMix.newSMGen >>= fmap Wrap . newMVar
-- | Generate a Double in @[0, 1)@ range
nextDouble :: RandomGen -> IO Double
nextDouble gen = withGen gen $ SplitMix.nextDouble
nextWord64 :: RandomGen -> IO Word64
nextWord64 gen = withGen gen $ SplitMix.nextWord64
-- | Uniformly distributed int value in @[0, n)@ range
nextInt :: RandomGen -> Int -> IO Int
nextInt gen n
| n <= 0 = error "bound must be positive"
| otherwise = (\w -> fromIntegral w `mod` n) <$> nextWord64 gen
{-------------------------------------------------------------------------------
Random bytes
-------------------------------------------------------------------------------}
newtype RandomBytes = RandomBytes {
getRandomBytes :: ByteString
}
instance Show RandomBytes where
show = BS.Char8.unpack . BS.Base16.encode . getRandomBytes
nextBytes :: RandomGen -> Int -> IO RandomBytes
nextBytes _ 0 = return $ RandomBytes BS.empty
nextBytes gen n = modifyMVar (unwrap gen) $ \initGen -> do
-- Allocate enough memory for a whole number of Word64
ptr :: Ptr Word64 <- C.mallocBytes (numWords * 8)
let loop :: SMGen -> [Int] -> IO SMGen
loop g [] = return g
loop g (i:is) = do
let (w, g') = SplitMix.nextWord64 g
C.poke (ptr `plusPtr` i) w
loop g' is
g' <- loop initGen [0, 8 .. n - 1]
bs <- unsafePackMallocCStringLen (castPtr ptr, n)
return (g', RandomBytes bs)
where
numWords :: Int
numWords = (n - 1) `div` 8 + 1
{-------------------------------------------------------------------------------
Internal: wrap pure operations
-------------------------------------------------------------------------------}
withGen :: RandomGen -> (SMGen -> (a, SMGen)) -> IO a
withGen gen f = modifyMVar (unwrap gen) $ return . swap . f