haskell / random

Random number library
Other
53 stars 50 forks source link

WIP: Implement safetey around non-splittable PRNGs #94

Closed lehins closed 7 months ago

lehins commented 3 years ago

I think I figured out an elegant solution for differentiating splittable vs non-splittable PRNGs in a type safe way with very little breakage!!!

CC @idontgetoutmuch and @curiousleo We've discussed the problem here at some lengths: idontgetoutmuch/random#7

This is the change to the type class:

class RandomGen g where
  type Splittable g :: Bool
  type Splittable g = 'True
  ...
  split :: Splittable g ~ 'True => g -> (g, g)

Which means only instances for PRNGs that are non splittable need to be adjusted, eg:

instance RandomGen PureMT where
   type Splittable PureMT = TypeError ('Text "PureMT is not a splittable PRNG")
   split = error "System.Random.Mersenne.Pure: unable to split the mersenne twister"

which if you try to use split function, will give you a nice error message:

impossibleSplit :: IO (PureMT, PureMT)
impossibleSplit = split <$> newPureMT
    • PureMT is not a splittable PRNG
    • In the first argument of ‘(<$>)’, namely ‘split’
      In the expression: split <$> newPureMT
      In an equation for ‘impossibleSplit’:
          impossibleSplit = split <$> newPureMT
   |
Compilation failed.

Instead of this ugly runtime error:

*** Exception: System.Random.Mersenne.Pure: unable to split the mersenne twister
CallStack (from HasCallStack):
curiousleo commented 3 years ago

That looks very nice!

It does seem to break deriving on old LTS. But that looks fixable. Other than that, I don't see any downsides with this approach.

It may be worth circulating this on the libraries list: (1) this technique deserves to be more well-known, (2) perhaps the authors of PRNG implementations have feedback.

Shimuuar commented 3 years ago

That's cute hack!

I don't think it's possible without dropping either type family or GHC<8.2. Prior to 8.2 GHC could derive newtype instances for classes with associated types. See https://gitlab.haskell.org/ghc/ghc/-/issues/2721

lehins commented 3 years ago

We can manually provide those instances, they don't have to be derived.

idontgetoutmuch commented 3 years ago

Nice :)

lehins commented 2 years ago

I figured how to improve it even further.

class RandomGen g where
  type Splittable g :: Constraint
  type Splittable g =
    TypeError ('ShowType g ':<>: 'Text " is not a splittable RandomGen")
  ...
  split :: Splittable g => g -> (g, g)

Any generator instance that is actually splittable only has to override the TypeError with ():

instance RandomGen SM.SMGen where
  type Splittable SM.SMGen = ()
  ...
  split = SM.splitSMGen

Here what we get in ghci:

> data NoopGen = NoopGen
> :{
| instance RandomGen NoopGen where
|   genWord64 _ = (0, NoopGen)
|   split _ = error "Impossible"
| :}
> split NoopGen

<interactive>:8:1: error:
    • NoopGen is not a splittable RandomGen
    • In the expression: split NoopGen
      In an equation for ‘it’: it = split NoopGen

CC @treeowl You've expressed interest in this in #97, what do you think about this approach?

Naturally, this is a backwards incompatible solution, but was always meant to be a breaking change. It can be included in random-1.3.0 whenever that might happen.

lehins commented 7 months ago

Closing in favor of #160