Skip to content

Commit

Permalink
Merge pull request #149 from haskell/lehins/extend-frozen-gen
Browse files Browse the repository at this point in the history
Deprecate `RandomGenM` in favor of a more powerful `FrozenGen`
  • Loading branch information
lehins authored Nov 24, 2023
2 parents e182019 + bc58313 commit 9f92421
Show file tree
Hide file tree
Showing 5 changed files with 316 additions and 100 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# 1.3.0

* Move `thawGen` from `FreezeGen` into the new `ThawGen` type class. Fixes an issue with
an unlawful instance of `StateGen` for `FreezeGen`.
* Add `modifyGen` and `overwriteGen` to the `FrozenGen` type class
* Add `splitGen` and `splitMutableGen`
* Switch `randomM` and `randomRM` to use `FrozenGen` instead of `RandomGenM`
* Deprecate `RandomGenM` in favor of a more powerful `FrozenGen`
* Add `isInRange` to `UniformRange`: [#78](https://github.com/haskell/random/pull/78)
* Add default implementation for `uniformRM` using `Generics`:
[#92](https://github.com/haskell/random/pull/92)
Expand Down
102 changes: 87 additions & 15 deletions src/System/Random/Internal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ module System.Random.Internal
RandomGen(..)
, StatefulGen(..)
, FrozenGen(..)
, ThawedGen(..)
, splitGen
, splitMutableGen

-- ** Standard pseudo-random number generator
, StdGen(..)
Expand All @@ -40,7 +43,6 @@ module System.Random.Internal
-- ** Pure adapter
, StateGen(..)
, StateGenM(..)
, splitGen
, runStateGen
, runStateGen_
, runStateGenT
Expand All @@ -67,7 +69,7 @@ module System.Random.Internal

import Control.Arrow
import Control.DeepSeq (NFData)
import Control.Monad (when)
import Control.Monad (when, (>=>))
import Control.Monad.Cont (ContT, runContT)
import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.ST
Expand Down Expand Up @@ -285,26 +287,101 @@ class Monad m => StatefulGen g m where
{-# INLINE uniformShortByteString #-}



-- | This class is designed for stateful pseudo-random number generators that
-- can be saved as and restored from an immutable data type.
-- | This class is designed for mutable pseudo-random number generators that have a frozen
-- imutable counterpart that can be manipulated in pure code.
--
-- It also works great with frozen generators that are based on pure generators that have
-- a `RandomGen` instance.
--
-- Here are a few laws, which are important for this type class:
--
-- * Roundtrip and complete destruction on overwrite:
--
-- @
-- overwriteGen mg fg >> freezeGen mg = pure fg
-- @
--
-- * Modification of a mutable generator:
--
-- @
-- overwriteGen mg fg = modifyGen mg (const ((), fg)
-- @
--
-- * Freezing of a mutable generator:
--
-- @
-- freezeGen mg = modifyGen mg (\fg -> (fg, fg))
-- @
--
-- @since 1.2.0
class StatefulGen (MutableGen f m) m => FrozenGen f m where
{-# MINIMAL (modifyGen|(freezeGen,overwriteGen)) #-}
-- | Represents the state of the pseudo-random number generator for use with
-- 'thawGen' and 'freezeGen'.
--
-- @since 1.2.0
type MutableGen f m = (g :: Type) | g -> f

-- | Saves the state of the pseudo-random number generator as a frozen seed.
--
-- @since 1.2.0
freezeGen :: MutableGen f m -> m f
-- | Restores the pseudo-random number generator from its frozen seed.
freezeGen mg = modifyGen mg (\fg -> (fg, fg))
{-# INLINE freezeGen #-}

-- | Apply a pure function to the frozen pseudo-random number generator.
--
-- @since 1.3.0
modifyGen :: MutableGen f m -> (f -> (a, f)) -> m a
modifyGen mg f = do
fg <- freezeGen mg
case f fg of
(a, !fg') -> a <$ overwriteGen mg fg'
{-# INLINE modifyGen #-}

-- | Overwrite contents of the mutable pseudo-random number generator with the
-- supplied frozen one
--
-- @since 1.3.0
overwriteGen :: MutableGen f m -> f -> m ()
overwriteGen mg fg = modifyGen mg (const ((), fg))
{-# INLINE overwriteGen #-}

-- | Functionality for thawing frozen generators is not part of the `FrozenGen` class,
-- becase not all mutable generators support functionality of creating new mutable
-- generators, which is what thawing is in its essence. For this reason `StateGen` does
-- not have an instance for this type class, but it has one for `FrozenGen`.
--
-- Here is an important law that relates this type class to `FrozenGen`
--
-- * Roundtrip and independence of mutable generators:
--
-- @
-- traverse thawGen fgs >>= traverse freezeGen = pure fgs
-- @
--
-- @since 1.3.0
class FrozenGen f m => ThawedGen f m where
-- | Create a new mutable pseudo-random number generator from its frozen state.
--
-- @since 1.2.0
thawGen :: f -> m (MutableGen f m)

-- | Splits a pseudo-random number generator into two. Overwrites the mutable
-- pseudo-random number generator with one of the immutable pseudo-random number
-- generators produced by a `split` function and returns the other.
--
-- @since 1.3.0
splitGen :: (RandomGen f, FrozenGen f m) => MutableGen f m -> m f
splitGen = flip modifyGen split

-- | Splits a pseudo-random number generator into two. Overwrites the mutable wrapper with
-- one of the resulting generators and returns the other as a new mutable generator.
--
-- @since 1.3.0
splitMutableGen :: (RandomGen f, ThawedGen f m) => MutableGen f m -> m (MutableGen f m)
splitMutableGen = splitGen >=> thawGen


data MBA = MBA (MutableByteArray# RealWorld)

Expand Down Expand Up @@ -451,15 +528,10 @@ instance (RandomGen g, MonadState g m) => StatefulGen (StateGenM g) m where
instance (RandomGen g, MonadState g m) => FrozenGen (StateGen g) m where
type MutableGen (StateGen g) m = StateGenM g
freezeGen _ = fmap StateGen get
thawGen (StateGen g) = StateGenM <$ put g

-- | Splits a pseudo-random number generator into two. Updates the state with
-- one of the resulting generators and returns the other.
--
-- @since 1.2.0
splitGen :: (MonadState g m, RandomGen g) => m g
splitGen = state split
{-# INLINE splitGen #-}
modifyGen _ f = state (coerce f)
{-# INLINE modifyGen #-}
overwriteGen _ f = put (coerce f)
{-# INLINE overwriteGen #-}

-- | Runs a monadic generating action in the `State` monad using a pure
-- pseudo-random number generator.
Expand Down
104 changes: 75 additions & 29 deletions src/System/Random/Stateful.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE Trustworthy #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
Expand All @@ -29,11 +30,16 @@ module System.Random.Stateful
-- $interfaces
, StatefulGen(..)
, FrozenGen(..)
, RandomGenM(..)
, ThawedGen(..)
, withMutableGen
, withMutableGen_
, randomM
, randomRM
, splitGen
, splitMutableGen

-- ** Deprecated
, RandomGenM(..)
, splitGenM

-- * Monadic adapters for pure pseudo-random number generators #monadicadapters#
Expand Down Expand Up @@ -158,9 +164,10 @@ import System.Random.Internal
-- > [3,4,3,1,4,6,1,6,1,4]
--
-- Given a /pure/ pseudo-random number generator, you can run the monadic pseudo-random
-- number computation @rollsM@ in 'StateT', 'IO', 'ST' or 'STM' context by applying a
-- monadic adapter like 'StateGenM', 'AtomicGenM', 'IOGenM', 'STGenM' or 'TGenM' (see
-- [monadic-adapters](#monadicadapters)) to the pure pseudo-random number generator.
-- number computation @rollsM@ in 'Control.Monad.State.Strict.StateT', 'IO', 'ST' or 'STM'
-- context by applying a monadic adapter like 'StateGenM', 'AtomicGenM', 'IOGenM',
-- 'STGenM' or 'TGenM' (see [monadic-adapters](#monadicadapters)) to the pure
-- pseudo-random number generator.
--
-- >>> let pureGen = mkStdGen 42
-- >>> newIOGenM pureGen >>= rollsM 10 :: IO [Word]
Expand All @@ -177,9 +184,9 @@ import System.Random.Internal
-- ['System.Random.RandomGen': pure pseudo-random number generators]
-- See "System.Random" module.
--
-- ['StatefulGen': monadic pseudo-random number generators] These generators
-- mutate their own state as they produce pseudo-random values. They
-- generally live in 'StateT', 'ST', 'IO' or 'STM' or some other transformer
-- ['StatefulGen': monadic pseudo-random number generators] These generators mutate their
-- own state as they produce pseudo-random values. They generally live in
-- 'Control.Monad.State.Strict.StateT', 'ST', 'IO' or 'STM' or some other transformer
-- on top of those monads.
--

Expand All @@ -192,10 +199,10 @@ import System.Random.Internal
-- Pure pseudo-random number generators can be used in monadic code via the
-- adapters 'StateGenM', 'AtomicGenM', 'IOGenM', 'STGenM' and 'TGenM'
--
-- * 'StateGenM' can be used in any state monad. With strict 'StateT' there is
-- no performance overhead compared to using the 'RandomGen' instance
-- directly. 'StateGenM' is /not/ safe to use in the presence of exceptions
-- and concurrency.
-- * 'StateGenM' can be used in any state monad. With strict
-- 'Control.Monad.State.Strict.StateT' there is no performance overhead compared to
-- using the 'RandomGen' instance directly. 'StateGenM' is /not/ safe to use in the
-- presence of exceptions and concurrency.
--
-- * 'AtomicGenM' is safe in the presence of exceptions and concurrency since
-- it performs all actions atomically.
Expand All @@ -216,13 +223,16 @@ import System.Random.Internal
-- @since 1.2.0
class (RandomGen r, StatefulGen g m) => RandomGenM g r m | g -> r where
applyRandomGenM :: (r -> (a, r)) -> g -> m a
{-# DEPRECATED applyRandomGenM "In favor of `modifyGen`" #-}
{-# DEPRECATED RandomGenM "In favor of `FrozenGen`" #-}

-- | Splits a pseudo-random number generator into two. Overwrites the mutable
-- wrapper with one of the resulting generators and returns the other.
--
-- @since 1.2.0
splitGenM :: RandomGenM g r m => g -> m r
splitGenM = applyRandomGenM split
{-# DEPRECATED splitGenM "In favor of `splitGen`" #-}

instance (RandomGen r, MonadIO m) => RandomGenM (IOGenM r) r m where
applyRandomGenM = applyIOGen
Expand All @@ -249,7 +259,7 @@ instance RandomGen r => RandomGenM (TGenM r) r STM where
-- ([-74,37,-50,-2,3],IOGen {unIOGen = StdGen {unStdGen = SMGen 4273268533320920145 15251669095119325999}})
--
-- @since 1.2.0
withMutableGen :: FrozenGen f m => f -> (MutableGen f m -> m a) -> m (a, f)
withMutableGen :: ThawedGen f m => f -> (MutableGen f m -> m a) -> m (a, f)
withMutableGen fg action = do
g <- thawGen fg
res <- action g
Expand All @@ -266,8 +276,8 @@ withMutableGen fg action = do
-- 4
--
-- @since 1.2.0
withMutableGen_ :: FrozenGen f m => f -> (MutableGen f m -> m a) -> m a
withMutableGen_ fg action = fst <$> withMutableGen fg action
withMutableGen_ :: ThawedGen f m => f -> (MutableGen f m -> m a) -> m a
withMutableGen_ fg action = thawGen fg >>= action


-- | Generates a list of pseudo-random values.
Expand Down Expand Up @@ -301,8 +311,9 @@ uniformListM n gen = replicateM n (uniformM gen)
-- 0.6268211351114487
--
-- @since 1.2.0
randomM :: (Random a, RandomGenM g r m) => g -> m a
randomM = applyRandomGenM random
randomM :: forall a g m. (Random a, RandomGen g, FrozenGen g m) => MutableGen g m -> m a
randomM = flip modifyGen random
{-# INLINE randomM #-}

-- | Generates a pseudo-random value using monadic interface and `Random` instance.
--
Expand All @@ -321,8 +332,9 @@ randomM = applyRandomGenM random
-- 2
--
-- @since 1.2.0
randomRM :: (Random a, RandomGenM g r m) => (a, a) -> g -> m a
randomRM r = applyRandomGenM (randomR r)
randomRM :: forall a g m. (Random a, RandomGen g, FrozenGen g m) => (a, a) -> MutableGen g m -> m a
randomRM r = flip modifyGen (randomR r)
{-# INLINE randomRM #-}

-- | Wraps an 'IORef' that holds a pure pseudo-random number generator. All
-- operations are performed atomically.
Expand Down Expand Up @@ -378,6 +390,13 @@ instance (RandomGen g, MonadIO m) => StatefulGen (AtomicGenM g) m where
instance (RandomGen g, MonadIO m) => FrozenGen (AtomicGen g) m where
type MutableGen (AtomicGen g) m = AtomicGenM g
freezeGen = fmap AtomicGen . liftIO . readIORef . unAtomicGenM
modifyGen (AtomicGenM ioRef) f =
liftIO $ atomicModifyIORef' ioRef $ \g ->
case f (AtomicGen g) of
(a, AtomicGen g') -> (g', a)
{-# INLINE modifyGen #-}

instance (RandomGen g, MonadIO m) => ThawedGen (AtomicGen g) m where
thawGen (AtomicGen g) = newAtomicGenM g

-- | Atomically applies a pure operation to the wrapped pseudo-random number
Expand Down Expand Up @@ -453,9 +472,18 @@ instance (RandomGen g, MonadIO m) => StatefulGen (IOGenM g) m where
instance (RandomGen g, MonadIO m) => FrozenGen (IOGen g) m where
type MutableGen (IOGen g) m = IOGenM g
freezeGen = fmap IOGen . liftIO . readIORef . unIOGenM
modifyGen (IOGenM ref) f = liftIO $ do
g <- readIORef ref
let (a, IOGen g') = f (IOGen g)
g' `seq` writeIORef ref g'
pure a
{-# INLINE modifyGen #-}
overwriteGen (IOGenM ref) = liftIO . writeIORef ref . unIOGen
{-# INLINE overwriteGen #-}

instance (RandomGen g, MonadIO m) => ThawedGen (IOGen g) m where
thawGen (IOGen g) = newIOGenM g


-- | Applies a pure operation to the wrapped pseudo-random number generator.
--
-- ====__Examples__
Expand Down Expand Up @@ -513,6 +541,16 @@ instance RandomGen g => StatefulGen (STGenM g s) (ST s) where
instance RandomGen g => FrozenGen (STGen g) (ST s) where
type MutableGen (STGen g) (ST s) = STGenM g s
freezeGen = fmap STGen . readSTRef . unSTGenM
modifyGen (STGenM ref) f = do
g <- readSTRef ref
let (a, STGen g') = f (STGen g)
g' `seq` writeSTRef ref g'
pure a
{-# INLINE modifyGen #-}
overwriteGen (STGenM ref) = writeSTRef ref . unSTGen
{-# INLINE overwriteGen #-}

instance RandomGen g => ThawedGen (STGen g) (ST s) where
thawGen (STGen g) = newSTGenM g


Expand Down Expand Up @@ -608,6 +646,16 @@ instance RandomGen g => StatefulGen (TGenM g) STM where
instance RandomGen g => FrozenGen (TGen g) STM where
type MutableGen (TGen g) STM = TGenM g
freezeGen = fmap TGen . readTVar . unTGenM
modifyGen (TGenM ref) f = do
g <- readTVar ref
let (a, TGen g') = f (TGen g)
g' `seq` writeTVar ref g'
pure a
{-# INLINE modifyGen #-}
overwriteGen (TGenM ref) = writeTVar ref . unTGen
{-# INLINE overwriteGen #-}

instance RandomGen g => ThawedGen (TGen g) STM where
thawGen (TGen g) = newTGenM g


Expand Down Expand Up @@ -761,19 +809,17 @@ applyTGen f (TGenM tvar) = do
--
-- === @FrozenGen@
--
-- `FrozenGen` gives us ability to use any stateful pseudo-random number generator in its
-- immutable form, if one exists that is. This concept is commonly known as a seed, which
-- allows us to save and restore the actual mutable state of a pseudo-random number
-- generator. The biggest benefit that can be drawn from a polymorphic access to a
-- stateful pseudo-random number generator in a frozen form is the ability to serialize,
-- deserialize and possibly even use the stateful generator in a pure setting without
-- knowing the actual type of a generator ahead of time. For example we can write a
-- function that accepts a frozen state of some pseudo-random number generator and
-- produces a short list with random even integers.
-- `FrozenGen` gives us ability to use most of stateful pseudo-random number generator in
-- its immutable form, if one exists that is. The biggest benefit that can be drawn from
-- a polymorphic access to a stateful pseudo-random number generator in a frozen form is
-- the ability to serialize, deserialize and possibly even use the stateful generator in a
-- pure setting without knowing the actual type of a generator ahead of time. For example
-- we can write a function that accepts a frozen state of some pseudo-random number
-- generator and produces a short list with random even integers.
--
-- >>> import Data.Int (Int8)
-- >>> :{
-- myCustomRandomList :: FrozenGen f m => f -> m [Int8]
-- myCustomRandomList :: ThawedGen f m => f -> m [Int8]
-- myCustomRandomList f =
-- withMutableGen_ f $ \gen -> do
-- len <- uniformRM (5, 10) gen
Expand Down
2 changes: 1 addition & 1 deletion test/Spec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ main =
, uniformSpec (Proxy :: Proxy (Word8, Word16, Word32, Word64, Word))
, uniformSpec (Proxy :: Proxy (Int8, Word8, Word16, Word32, Word64, Word))
, uniformSpec (Proxy :: Proxy (Int8, Int16, Word8, Word16, Word32, Word64, Word))
, Stateful.statefulSpec
, Stateful.statefulGenSpec
]

floatTests :: TestTree
Expand Down
Loading

0 comments on commit 9f92421

Please sign in to comment.