Recently I was thinking about a programming problem that would need access to random values. I thought it might be fun to write up my though process as an advanced Haskeller while working through this particular problem.
In Haskell, one would write such a program by using a random monad to access an oracle providing random numbers.
The traditional way to implement MonadRandom
is using the state monad.
The Gen
type holds the state of the (pseudo-)random number generator, and the randomInt
function returns a new random number and updates the state of the generator.
type Random a = State Gen a randomInt :: Gen -> (Int,Gen) randomOracle :: MonadRandom Int randomOracle = state random
Then I write my program inside the Random monad, making calls to the randomOracle
as needed
myProg :: Random Result myProg = do {- ... -} x <- randomOracle {- ... -} y <- randomOracle {- ... -}
In order to run my program, I need to provide it with a random seed.
evalRandom :: Random result -> Gen -> result evalRandom = evalState
For deterministic testing, we can pass fixed generators to evalRandom
.
If we use StdGen
, we can map our Random
program to IO
and use the system random number generator.
type Gen = System.Random.StdGen randomInt = System.Random.random evalRandomIO :: Random result -> IO result evalRandomIO = getStdRandom . runState
For the most general possible random number generator, the type for the generator state is simply an infinite stream of random values.
data Stream a = Cons a (Stream a) unfoldStream :: (g -> (a, g)) -> g -> Stream a unfoldStream next = go where go seed = Cons value (go nextSeed) where (value, nextSeed) = next seed type Gen = Stream Int randomInt (Cons hd tl) = (hd, tl) evalRandomStdGen :: Random result -> StdGen -> result evalRandomStdGen rr = evalRandom rr . unfoldStream System.Random.random evalRandomIO :: Random result -> IO result evalRandomIO rr = evalRandomStdGen rr <$> newStdGen
Before, when I was an intermediate Haskeller, I would probably stop at this point pretty satisfied with this.
And let me be clear that this is a fine solution.
However, now that I am an advanced Haskeller, I cannot help but feel a little dissatisfied with this solution.
The problem with this implementation of the Random
monad is that the type is too broad.
Since the Random
type is the State
monad, there are operations allowed by the type that should not be allowed for a Random
program.
For instance, within the Random
type, someone could store the state of the generator and restore it later causing random values to be replayed, or someone might completely replace the state of the generator with their own value.
One way to solve this problem is to use Haskell’s module system to abstract the monad and only expose the randomOracle
operation.
While this is a reasonable solution (in fact it is a very good solution as we will see), it would be nicer if we could instead use the type system to create a monad that is only capable of representing the programs we want to allow, and disallows other programs that would try manipulate the generator state in ways we do not want.
Essentially we want our Random
programs to only be able to query the random oracle, and that is it.
After reflecting on this problem and the various kinds of monads I know about, I realized that the a suitable free monad captures exactly this notion of providing only an operation to query the random oracle.
Specifically we want Control.Monad.Free.Free (Reader Int)
or more directly (but more obtusely written) as Control.Monad.Free.Free ((->) Int)
.
We truly want a free monad because any sequence of responses from the random oracle is valid.
One problem with this Free
monad is that the bind operation can be slow because it needs to traverse through a, possibly long, data structure.
There are several solutions to this, but for this particular free monad, I happen to know that the Van Laarhoven free monad representation is possible:
The type forall m. Monad m => m Int -> m a
is isomorphic to Control.Monad.Free.Free ((->) Int) a
.
newtype Random a = Random { instantiateRandom :: forall m. Monad m => m Int -> m a } instance Monad Random where return a = Random $ \_ -> return a ma >>= mf = Random . runReaderT $ ReaderT (instantiateRandom ma) >>= ReaderT . instantiateRandom . mf instance Applicative Random where pure = return (<*>) = ap instance Functor Random where fmap = liftM
In this representation, the random oracle function just fetches the argument.
randomOracle :: Random Int randomOracle = Random id
For deterministic testing purposes we can evaluate the random monad by instantiating it with our state monad from before.
evalRandom :: Random result -> Stream Int -> result evalRandom rr = evalState . instantiateRandom rr . state $ \(Cons hd tl) -> (hd, tl)
However, we can also directly instantiate it with the IO
monad for production purposes.
evalRandomIO :: Random result -> IO result evalRandomIO rr = instantiateRandom rr evalRandomIO
This is all very good; however, the advanced Haskeller in me still thinks that I ought to be able to write evalRandom
without the need to invoke the State
monad.
This is because if we were actually using the Free ((->) Int
monad, I would be writing evalRandom
using iterA
.
iterA :: (Applicative p, Functor f) => (f (p a) -> p a) -> Free f a -> p a evalFreeRandom :: Free ((->) Int) result -> Stream Int -> result evalFreeRandom = iterA (\fpa (Cons hd tl) -> fpa hd tl)
No need for any state monad business in evalFreeRandom
.
How can I get that elegant solution with the Van Laarhoven free monad?
One way would be to convert to the Free ((->) Int)
representation
freeRandom :: Random result -> Free ((->) Int) result freeRandom rr = instantiateRandom rr (liftF id) evalRandom :: Random result -> Stream Int -> result evalRandom = evalFreeRandom . freeRandom
Surely there must be a way to do this directly?
Before solving this I turned to another interesting problem.
The iterA
function recurses over the free monad structure to do its evaluation.
The Free
monad comes with its own general recursive elimination function called foldFree
foldFree :: Monad m => (forall x. f x -> m x) -> Free f a -> m a
This foldFree
function is captures everything about the free monad, so it should be possible to write iterA
by using foldFree
to do all the recursion.
But how is that even possible?
The types of foldFree
and iterA
look too far apart.
foldFree
requires an natural transformation as input, and the arguments to iterA
provide nothing resembling that.
To solve this I turned to the #haskell IRC channel for help.
With assistance from glguy, ReinH, and byorgey, I turned the well known idea that if you want turn something to or from a natural transformation you use some sort of Yoneda / continuation like construction.
In this particular case, the Cont (p a)
monad seems to capture what we need.
Following the types (and forgetting about the semantics) we end up the following.
iterA :: (Applicative p, Functor f) => (f (p a) -> p a) -> Free f a -> p a iterA h ff = runCont (foldFree (\fx -> Cont $ \k -> h (k <$> fx)) ff) pure
As an aside, glguy has a more “natural” solution, but it technically has a less general type, so I will not talk about here, even if I do feel it is better.
Turning back to our original problem of directly writing evalRandom
without using the state monad, we can try to see if Cont
will solve our problem.
evalRandom :: Random result -> Stream Int -> result evalRandom rr = runCont (instantiateRandom rr (Cont $ \k (Cons hd tl) -> k hd tl)) const
We can compare the Cont
solution to the State
solution and see that the code is pretty similar.
evalRandom :: Random result -> Stream Int -> result evalRandom rr = evalState (instantiateRandom rr (state $ \(Cons hd tl) -> (hd, tl)))
The inner Cont
construction looks very similar to the inner state
construction.
The outer call to const
in the Cont
solution discards the final "state" which captures the same functionality that evalState
has for the State
solution.
Now we can ask, which has better performance, the State
solution, with its tupling and untupling of values, or the Cont
solution which uses continuations instead?
I will leave it to the GHC experts to figure that one out.
Arguably most of this is an exercise in academics, but it only took me a hour or three to go through this whole thought process. As an advanced Haskeller, I have slowly gathered, over many years, experience with these sorts of abstractions so that it starts becoming easy to do this sort of reasoning. While it may or may not matter much for my particular application, eventually this kind of reasoning becomes important. For example, the modern stream fusion in GHC exploits constructions that resemble this kind of reasoning, and that has had a big impact on performance.
For the non-advanced Haskellers out there, do not be deterred. Keep practicing your craft; keep reading about new abstractions, even if you do not fully get it; keep looking out for potential applications of those abstractions to solidify your understanding. Eventually you will have lots of very powerful problem solving tools at your disposal for making safe software.