Search code examples
haskellfunctional-programmingmonadspurely-functional

Randomness in a nested pure function


I want to provide a function that replaces each occurrence of # in a string with a different random number. In a non-pure language, it's trivial. However, how should it be designed in a pure language? I don't want to use unsafePerformIO, as it rather looks like a hack and not a proper design.

Should this function require a random generator as one of its parameters? And if so, would that generator have to be passed through the whole stack of invocations? Are there other possible approaches? Should I use the State monad, here? I would appreciate a toy example demonstrating a viable approach...


Solution

  • You would, in fact, use a variant of the state monad to pass the random generator around behind the scenes. The Rand type in Control.Monad.Random helps with this. The API is a bit confusing, but more because it's polymorphic over the type of random generator you use than because it has to be functional. This extra bit of scaffolding is useful, however, because you can easily reuse your existing code with different random generators which lets you test different algorithms as well as explicitly controlling whether the generator is deterministic (good for testing) or seeded with outside data (in IO).

    Here's a simple example of Rand in action. The RandomGen g => in the type signature tells us that we can use any type of random generator for it. We have to explicitly annotate n as an Int because otherwise GHC only knows that it has to be some numeric type that can be generated and turned into a string, which can be one of multiple possible options (like Double).

    randomReplace :: RandomGen g => String -> Rand g String
    randomReplace = foldM go ""
      where go str '#' = do
              n :: Int <- getRandomR (0, 10)
              return (str ++ show n)
            go str chr = return $ str ++ [chr]
    

    To run this, we need to get a random generator from somewhere and pass it into evalRand. The easiest way to do this is to get the global system generator which we can do in IO:

    main :: IO ()
    main = do gen <- getStdGen
              print $ evalRand (randomReplace "ab#c#") gen
    

    This is such a common pattern that the library provides an evalRandIO function which does it for you:

    main :: IO ()
    main = do res <- evalRandIO $ randomReplace "ab#c#"
              print res
    

    In the end, the code is a bit more explicit about having a random generator and passing it around, but it's still reasonably easy to follow. For more involved code, you could also use RandT, which allows you to extend other monads (like IO) with the ability to generate random values, letting you relegate all the plumbing and setup to one part of your code.