Make Frozen type family again#67
Make Frozen type family again#67Shimuuar wants to merge 1 commit intoidontgetoutmuch:interface-to-performancefrom
Conversation
Purpose of Frozen is to provide immutable snapshot of generator state from which generator could be restored. As such it shouldn't carry state token, thus it couldn't be injective and could only be implemented as a type family. Current design makes it impossible to implement following patterns generically: > runST $ do do_something_with g > freezeGen g > frozen <- readGeneratorState > return $ runST $ do g <- thawGen > do_something_with g This change does make type inference worse. But this is acceptable price to pay in order to have useful Frozen.
|
@Shimuuar I understand the fix, but I am still having trouble with the problem you are describing. Can you comment here a simple example that compiles and produces some result with your fix, while being impossible without it? |
|
There is a good chance that I don't understand your concern correctly, but if I do, then my opinion is that there is no problem there because we don't have a generic way to construct the frozen state, therefore it is irrelevant that it depends on the state token. In particular in your example Here is a way anyone can use it with MWC. When compared to this PR there would be no wrapper/unwrapper for |
Here is simplest example that doesn't work. Just trying to pass frozen state to function causes. Idea is to load pure PRNG state from somewhere and then run some otherwise pure computation: broken :: Frozen (MutGen s StdGen) -> [Word64]
broken frozen = runST $ do
g <- thawGen frozen
replicateM 4 $ uniformWord64 gIt fails with There's rather ugly workaround. Wrap that thing in a box. And one will have to carry such box around. newtype Box = Box (forall s. Frozen (MutGen s StdGen))
boxed :: Box -> [Word64]
boxed (Box frozen) = runST $ do
g <- thawGen frozen
replicateM 4 $ uniformWord64 gVariant with type family works. But it require type annotations works :: StdGen -> [Word64]
works frozen = do
runST $ do
g :: MutGen s StdGen <- thawGen frozen
replicateM 4 $ uniformWord64 gAlternativesBut as soon as we drop that that annoying type variable everything becomes conveniently injective and won't require any annotations. Maybe we got things backwards. Instead of trying to derive frozen state from mutable state which require discarding state tyvar we should derive mutable generator from its frozen state. After all mutable generator could be reduced to pure one by fthawing and then freezing generator. Efficiency will be horrible of course. This approach suffers from same problem as dispatching on type constructor. Where should we get type variable for state token? But this approach could work out. |
|
That's the point that doesn't make sense to me. Why would you ever need that? broken :: Frozen (MutGen s StdGen) -> [Word64]This works without any type annotations: notBroken :: StdGen -> [Word64]
notBroken seed = runST $ do
g <- thawGen (MutGen seed)
replicateM 4 $ uniformWord64 g |
|
I am yet to see a compelling reason for this PR. |
|
Hell, even this would work just fine, if you really wanted the original type signature notBroken2 :: Frozen (MutGen s StdGen) -> [Word64]
notBroken2 (MutGen seed) = runST $ do
g <- thawGen (MutGen seed)
replicateM 4 $ uniformWord64 gSo, my question is, is there a really impossible case that requires |
|
Newtype wrapper break down the moment you want to be polymorphic in generator type. But you managed to convince me that both variants are bad. What we really want from Frozen is to be able to write: something along the lines: |
I think you are trying to say what we want is something like that, right? Frozen is irrelevant here and something else prevents us form doing that, namely the So, |
|
@Shimuuar I think I might have a pretty nasty, but really cool solution to this. I am gonna mess with it a bit and will ping you when I have something working :) |
|
@Shimuuar Nevermind, spoke too soon :) I don't think it is possible to solve this with Just in case you are curious, here is what I had in mind. By the way, this approach does get rid of class RandomGenM (g :: (* -> *) -> *) where
type GenMonad g :: (* -> *) -> Constraint
data Frozen g :: *
{-# MINIMAL freezeGen,thawGen,(uniformWord32R|uniformWord32),(uniformWord64R|uniformWord64) #-}
thawGen :: (GenMonad g m, Monad m) => Frozen g -> m (g m)
freezeGen :: (GenMonad g m, Monad m) => g m -> m (Frozen g)
uniformWord32R :: (GenMonad g m, Monad m) => Word32 -> g m -> m Word32
uniformWord64R :: (GenMonad g m, Monad m) => Word64 -> g m -> m Word64
uniformWord8 :: (GenMonad g m, Monad m) => g m -> m Word8
uniformWord8 = fmap fromIntegral . uniformWord32R (fromIntegral (maxBound :: Word8))
uniformWord16 :: (GenMonad g m, Monad m) => g m -> m Word16
uniformWord16 = fmap fromIntegral . uniformWord32R (fromIntegral (maxBound :: Word16))
uniformWord32 :: (GenMonad g m, Monad m) => g m -> m Word32
uniformWord32 = uniformWord32R maxBound
uniformWord64 :: (GenMonad g m, Monad m) => g m -> m Word64
uniformWord64 = uniformWord64R maxBound
uniformByteArray :: (GenMonad g m, Monad m) => Int -> g m -> m ByteArray
default uniformByteArray :: (GenMonad g m, PrimMonad m) => Int -> g m -> m ByteArray
uniformByteArray = uniformByteArrayPrim
withGenM :: (Monad m, RandomGenM g, GenMonad g m) => Frozen g -> (g m -> m a) -> m (a, Frozen g)
withGenM fg action = do
g <- thawGen fg
res <- action g
fg' <- freezeGen g
pure (res, fg')
data PureGen g (m :: * -> *) = PureGenI
instance (RandomGen g) => RandomGenM (PureGen g) where
type GenMonad (PureGen g) = MonadState g
newtype Frozen (PureGen g) = PureGen g
thawGen (PureGen g) = PureGenI <$ put g
freezeGen _ = fmap PureGen get
uniformWord32R r _ = state (genWord32R r)
uniformWord64R r _ = state (genWord64R r)
uniformWord8 _ = state genWord8
uniformWord16 _ = state genWord16
uniformWord32 _ = state genWord32
uniformWord64 _ = state genWord64
uniformByteArray n _ = state (genByteArray n)
newtype MutGen g m = MutGenI (MutVar (PrimState m) g)
instance RandomGen g => RandomGenM (MutGen g) where
type GenMonad (MutGen g) = PrimMonad
newtype Frozen (MutGen g) = MutGen g
thawGen (MutGen g) = fmap MutGenI (newMutVar g)
freezeGen (MutGenI gVar) = fmap MutGen (readMutVar gVar)
uniformWord32R r = atomicMutGen (genWord32R r)
uniformWord64R r = atomicMutGen (genWord64R r)
uniformWord8 = atomicMutGen genWord8
uniformWord16 = atomicMutGen genWord16
uniformWord32 = atomicMutGen genWord32
uniformWord64 = atomicMutGen genWord64
uniformByteArray n = atomicMutGen (genByteArray n)
atomicMutGen :: PrimMonad m => (g -> (a, g)) -> MutGen g m -> m a
atomicMutGen op (MutGenI gVar) =
atomicModifyMutVar' gVar $ \g ->
case op g of
(a, g') -> (g', a) |
|
@Shimuuar I am closing this PR due to your comment:
If I misunderstood you and if you still think type vs data is still a good idea, please reopen it and we can pickup this discussions again. |
I know that feeling. I have small graveyard of ideas that didn't quite work. But I finally got a design with which I fought last few week. Here is rough draft: https://gist.github.com/Shimuuar/353a5b3367cf410677843028ba9fb57a Its main selling point is ability to use mutability and hide this fact so it becomes possible to write things like: |
|
It is certainly a fun experiment, but it is way more complex than what we have right now. It also has real problems, in particular:
@Shimuuar Let me ask you, what does this approach solve, that we cannot do right now? Also if it makes something easier, or clearer (which pretty subjective, but arguable) it would be nice to have a side by side example to what we have already implemented. I don't think hiding mutability should be the desired goal of a genByteString :: RandomGen g => Int -> g -> (ByteString, g)
genByteString n g = runPureGenST g (uniformByteStringPrim n)which produces a If passing around generator is a problem I think #70 is a much better solution. This #70 (comment) also shows usage of |
True. But it's not a complete proposal. What I have in mind is something in same spirit as #70 which lifts to
Necessary.
In any case this is proof of concept. I think it will fit well into random but it's not yet tested |
Purpose of Frozen is to provide immutable snapshot of generator state from which
generator could be restored. As such it shouldn't carry state token, thus it
couldn't be injective and could only be implemented as a type family.
Current design makes it impossible to implement following patterns generically:
This change does make type inference worse. But this is acceptable price to pay
in order to have useful Frozen.
Alternatives
However it's possible to restore injectivity if we will dispatch over type constuctor, e.g,.
MWC.Geninstead ofMWC.Gen s. This will also require adding phantom parameter to PureGen:data PureGen g s = PureGen. But this design is not without problems since stateful generators requires thats ~ PrimState mand for pure generatorssis not constrained at all.These problem could be worked around but results could end up rather hairy. In any case I didn't explore this particular design