Skip to content

Add an API to serialise/deserialise to/from disk #123

Description

@adinapoli

Up until random-1.1 a barebone (and potentially brittle) way to serialise and deserialise a StdGen would have been to use show and read, however random-1.2.0 removed Read (for good reasons) which means this is not possible anymore. As a consequence, writing things like a Serialize instance for StdGen is not possible at the moment.

Technically speaking one can work around this by using the .Internal module and simply use seedSMGen' and unseedSMGen on the underlying SMGen, but it feels wrong to use the Internal module and to rely on the concrete implementation of StdGen.

In a nutshell, it would be nice to have two functions as part of the API similar to the following:

toSeedGamma :: StdGen -> (Word64, Word64)
fromSeedGamma :: (Word64, Word64) -> StdGen

Thanks!

Activity

  1. lehins commented on Sep 30, 2021

    @lehins
    Contributor

    This is a reasonable request and in fact I wanted it myself for some time. However, I think it would be better to provide a general interface for this, in case other PRNGs need such functionality. Maybe something along the lines of:

    class RandomGen g where
      data Seed g :: Type
      toSeed :: g -> Seed g
      fromSeed :: Seed g -> g
      ...

    this would allow for StdGen implementation to be

    instance RandomGen StdGen where
      data Seed StdGen = StdGenSeed !Word64 !Word64
      toSeed (StdGen smGen) = uncurry StdGenSeed $ SM.unseedSMGen smGen
      fromSeed (StdGenSeed seed gamma) = StdGen $ SM.seedSMGen seed gamma
      ...

    Thoughts?

  2. adinapoli commented on Sep 30, 2021

    @adinapoli
    Author

    Yes, I think that something along those lines should work, thanks!

  3. adamgundry commented on Oct 1, 2021

    @adamgundry
    Member

    What do you actually gain from adding this to RandomGen, i.e. what is the conceptual difference between g and Seed g? I suppose it would make it possible to write code that is polymorphic in e.g. a Serialize (Seed g) constraint for some Serialize class, but you could just as well use Serialize g directly.

    EDIT: I suppose it could be useful if instead of an associated data family, toSeed/fromSeed actually converted to some kind of primitive representation (e.g. some flavour of byte string).

  4. lehins commented on Oct 1, 2021

    @lehins
    Contributor

    @adamgundry You are right, the suggested interface is not powerful enough to be useful.

    It would be nice to have general ability to initialize any PRNG from a seed, say if we were to provide some ability to draw entropy from the system in a form of a ByteString. For that we would need information from g on how many bytes it needs.

    Another thing we'd like, as this ticket suggest, is the ability to serialize any g to file and back. However, asking for just a string of bytes is not sufficient in my books, we need some type safety. How about this for an addition to the interface:

    newtype BytesN (n :: Nat) = BytesN ShortByteString
    
    toBytesN :: forall n. KnownNat n => ShortByteString -> Maybe (BytesN n)
    fromBytesN :: BytesN n -> ShortByteString
    
    class RandomGen g where
      type SeedSize g :: Nat
      toSeed :: g -> BytesN (SeedSize g)
      fromSeed :: BytesN (SeedSize g) -> g

    This would allow serialization libraries to provide instances for any PRNG regardless of the underlying representation. But most importantly it would allow us to make it opt in and off by default, making it backwards compatible:

    class RandomGen g where
      type SeedSize g = TypeError (ShowType g :<>: Text " doesn't support saving seeds")
      toSeed :: g -> BytesN (SeedSize g)
      toSeed _ = error "Impossible: Not supported"
      fromSeed :: BytesN (SeedSize g) -> g
      fromSeed _ = error "Impossible: Not supported"

    which would produce a type error on toSeed/fromSeed instead of some ugly runtime error.

  5. lehins commented on Feb 3, 2024

    @lehins
    Contributor

    For anyone interested in this functionality there is an implementation in #162 that is still lacking some tests, but already works quite nicely. Here is an example function that should depict the power of this new SeedGen interface quite nicely:

    withSeedFile :: (SeedGen g, MonadIO m) => FilePath -> (g -> m (a, g)) -> m a
    withSeedFile fileName f = do
    bs <- liftIO $ BS.readFile fileName
    seed <- liftIO $ mkSeedFromByteString bs
    (res, gen) <- f $ seedGen seed
    liftIO $ BS.writeFile fileName $ unSeedToByteString $ unseedGen gen
    pure res

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions