Save and restore random number generator (freeze, thaw?)

Hi! I am trying to save and restore a random number generator using the new System.Random (random 1.2) interface. I am failing completely, because I do not understand the types of freezeGen and thawGen (but maybe I do not even need those).

Ideally I want something like:

f :: IO ()
f = do
  g <- newIOGenM $ mkStdGen 42
  doALotOfFunStuffWithG g
  s <- saveGenerator g
  print s

This should print the current seed (or seed tuple, as far as I can tell form looking at splitmix), and newIOGenM $ mkStdGen s should be the same g (when it was saved).

Thanks for your help.

An IOGenM will be a newtype around an IO ref. You can call unIOGenM to get the ref, then read the ref with standard functions, print it out, save it, etc. In turn, that extracted value can be passed to newIOGenM again to create another one. That value will already be a stdgen, so there will be no need to call mkStdGen on it again, just newIOGenM.

Note that you do not need to use any of the stateful machinery with freezing and thawing to do what you want – just use the pure interface, and pass around and manage the generator directly, if you prefer!

Thank you! Directly using the IORef is a good idea!

I like the stateful interface, I just never have used freeze nor thaw, and I thought they are here to save and restore the generator. But obviously they are not… What do they do?

A function to save a generator should be part of the library interface though (stateful or not). I do not think that it should be necessary to tinker around with IORefs to save a generator. What do you think?

A generator is just a value. You can save it however you like. The stateful interface just wraps up the generator in a ref (IO or ST or otherwise) so you can use it in a monadic fashion. Freeze and thaw do not save and restore the generator to disk or the like – they extract it from the monadic context, and they put it back into that context.

In fact, looking more closely, you’re not using that monadic context at all – you’re using IOGenM directly – this isn’t related to the use of freeze and thaw, which is for use in a random context monad (and in that case they don’t work with an IOGenM they take and yield a plain IOGen).

So freeze and thaw are unrelated to anything you’re doing – they’re for a different interface, one where you’re using some “random monad”. Instead, you’re just using the IO monad, and for some reason keeping your generator wrapped in an IO ref.

I still believe that saving a generator (or a seed) should be easier to achieve, even in the stateful interface.

Thank you for your explanations. That is what I thought, but I still do not know what freeze and thaw are doing. What is a “random context monad”?

I do need IO in some of my random computations, and it is inconvenient to pass the updated generator around in the non-stateful interface, that’s why I needed IOGenM. What is the difference between IOGenM and IOGen.

In my opinion, the documentatin of the random package should be improved. I do not understand parts of the documentation well, and I consider myself an advanced user of Haskell. I am happy to help in doing that. Your few paragraps make a lot of things already much clearer to me.

EDIT: Saving the seed looks like so:

import System.Random.Internal
import System.Random.SplitMix
import System.Random.Stateful

saveGen' :: IOGenM StdGen -> IO (Word64, Word64)
saveGen' (IOGenM r) = do
  (StdGen g) <- readIORef r
  pure $ unseedSMGen g

That’s a bit verbose :grinning_face_with_smiling_eyes: (also note the Internal import).

I’m sure the authors would appreciate some PRs helping to flesh out the documentation.

In fact, looking through it a second time, I see that while my advice worked for you, it probably isn’t exactly the way they intended things to be done, and the freeze/thaw stuff may indeed be more relevant than I thought.

In fact, freezeGen should, on an IOGenM, yield an IOGen, via doing both the unpacking step and the reading the IORef step:

I confess I don’t understand why they chose to use newtype wrappers on both sides of the type class – I would think that using only one or the other side (at most!) would suffice.

Right! I also realized that freeze is doing the readIORef. But I had trouble with the types when using it. I just couldn’t please the type checker. The function I provide above is more specific in terms of types, but it works!

EDIT: I think part of the reason why they use newtypes on both ends is that every generator generates a different “freeze type”. When freezing an SMGen, you get a (Word64, Word64), when freezing the generator of MWC, you get a Vector Word32.

I do not understand why it is called freeze and thaw. I would call it save and restore.

The origin of the freeze and thaw prefixes can be found in the paper State in Haskell, where the operations enabled more efficient operations on aggregate data e.g. arrays:

  • freeze... :: {- mutable type -} -> monadic {- constant type -}
  • thaw... :: {- constant type -} -> monadic {- mutable type -}

…it wasn’t really intended for saving and loading e.g. to an external format in a file.

It’s seems odd that System.Random doesn’t already have some well-documented way of doing this - in that case, a polite inquiry (or even an offer of help) to the devs
could prove useful…

Thank you!

I created a ticket: Maybe we get some more opinions like so.

I think a lot of these questions about thaw and freeze are answered in my blog post: New random interface | Alexey Kuleshevich

Also I did a talk at HaskellerZ meetup on the same topic: Alexey Kuleshevich - Haskell's new random package interface - January 2021 - HaskellerZ - YouTube

With respect to storing and saving generator from disk, we do need that functionality and there is already a ticket about it: