miso-1.12.0.0: src/Miso/Random.hs
-----------------------------------------------------------------------------
{-# LANGUAGE CPP #-}
-----------------------------------------------------------------------------
-- |
-- Module : Miso.Random
-- Copyright : (C) 2016-2026 David M. Johnson
-- License : BSD3-style (see the file LICENSE)
-- Maintainer : David M. Johnson <code@dmj.io>
-- Stability : experimental
-- Portability : non-portable
--
-- = Overview
--
-- "Miso.Random" provides a pseudo-random number generator for miso
-- components and their test infrastructure. It is built on the
-- <https://prng.di.unimi.it/ SplitMix32> algorithm, implemented as a
-- stateful JavaScript function stored in a 'Miso.DSL.Function'.
--
-- Two usage styles are available:
--
-- * __Explicit generator__ — pass a 'StdGen' through your code using
-- 'next' (analogous to @System.Random@).
-- * __Global generator__ — use 'replicateRM' or access 'globalStdGen'
-- directly for fire-and-forget random values.
--
-- = Quick start
--
-- @
-- import "Miso.Random"
--
-- -- Explicit generator
-- example :: IO ()
-- example = do
-- gen <- 'newStdGen'
-- let (v, g') = 'next' gen -- v :: Double in [0, 1)
-- print v
--
-- -- Global generator (convenience)
-- tenValues :: IO [Double]
-- tenValues = 'replicateRM' 10
--
-- -- Reproducible seed for tests
-- deterministicGen :: 'StdGen'
-- deterministicGen = 'mkStdGen' 42
-- @
--
-- = Seeding
--
-- * 'newStdGen' seeds from @crypto.getRandomValues()@ — cryptographically
-- random, non-reproducible.
-- * 'mkStdGen' takes an explicit 'Seed' (@Int@) — reproducible, useful for
-- property tests or simulations.
-- * 'globalStdGen' is seeded once at module load time from @Math.random()@.
--
-- = See also
--
-- * 'Miso.FFI.Internal.splitmix32' — the raw JS PRNG primitive
-- * 'Miso.FFI.Internal.getRandomValue' — @crypto.getRandomValues()@ used for seeding
----------------------------------------------------------------------------
module Miso.Random
( -- ** Types
StdGen (..)
, Seed
-- ** Functions
, newStdGen
, mkStdGen
, next
, replicateRM
, getStdGen
, setStdGen
-- ** Globals
, globalStdGen
) where
-----------------------------------------------------------------------------
import Data.Tuple (swap)
import Control.Monad.State (state, runState)
import Control.Monad (replicateM)
import Data.IORef
import System.IO.Unsafe (unsafePerformIO)
-----------------------------------------------------------------------------
import Miso.DSL
import qualified Miso.FFI.Internal as FFI
-----------------------------------------------------------------------------
-- | t'StdGen' holds a JS t'Function'.
newtype StdGen = StdGen Function
-----------------------------------------------------------------------------
-- | An initial 'Seed' value, useful for simulations or reproducing test failures
type Seed = Int
-----------------------------------------------------------------------------
-- | Like 'Miso.Random.newStdGen' but takes a t'Seed' as an argument and is pure.
mkStdGen
:: Seed
-- ^ Initial seed value; identical seeds produce identical sequences
-> StdGen
mkStdGen seed = StdGen $ Function $ unsafePerformIO $ FFI.splitmix32 (fromIntegral seed)
-----------------------------------------------------------------------------
-- | Create a new t'StdGen', defaulting to a random t'Seed'.
newStdGen :: IO StdGen
newStdGen = do
seed <- FFI.getRandomValue
StdGen . Function <$> FFI.splitmix32 seed
-----------------------------------------------------------------------------
-- | Get the next t'StdGen', extracting the value, useful with t'State'.
next
:: StdGen
-- ^ Current generator state
-> (Double, StdGen)
next (StdGen func) = unsafePerformIO $ do
result <- apply func ()
pure (result, StdGen func)
-----------------------------------------------------------------------------
-- | Global 'StdGen', used by 'replicateRM' and others.
globalStdGen :: IORef StdGen
{-# NOINLINE globalStdGen #-}
globalStdGen = unsafePerformIO $ do
seed <- floor . (*1e7) <$> FFI.mathRandom
newIORef (mkStdGen seed)
-----------------------------------------------------------------------------
-- | Read the `globalStdGen`
getStdGen :: IO StdGen
getStdGen = readIORef globalStdGen
-----------------------------------------------------------------------------
-- | Set the `globalStdGen`
setStdGen
:: StdGen
-- ^ New generator to install as the global PRNG
-> IO ()
setStdGen = atomicWriteIORef globalStdGen
-----------------------------------------------------------------------------
-- | Generate n amount of random numbers. Uses the global PRNG 'globalStdGen'.
--
-- @
-- replicateRM 10 :: IO [Double]
-- @
--
replicateRM
:: Int
-- ^ Number of random 'Double' values to generate in @[0, 1)@
-> IO [Double]
replicateRM n = do
atomicModifyIORef globalStdGen $ \gen -> do
swap $ flip runState gen $ replicateM n (state next)
-----------------------------------------------------------------------------