Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate RandomGenM in favor of a more powerful FrozenGen #149

Merged
merged 4 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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