Haskell Bits 1 - Randomness
Haskell Bits is a new series of bite-sized posts that I hope will empower people to “get it done and move on”, providing useful information and links to learn more if desired. I’ll be providing full
main files in each example (with imports!) to make porting this stuff into your own project as frictionless as possible. This first “Haskell Bit” will cover randomness.
You need at least two things to produce a random number:
- An initial “seed” value
- A pure function that produces a new number from that seed. (“RNG”)
That’s all for a single number.
Most programming languages will hide these details from you unless you need them. Most of the time, you can just call
random() and get a random number (typically between 0 and 1), using a seed value generated from some system variable that is always changing (current time in very small units is common).
The simplest way to replicate this behavior in Haskell is by using the
System.Random module, part of the
We can use
randomRIO to pull from a global RNG:
import System.Random main :: IO () main = do -- A random `Double` between 0 and 1 (randomIO :: IO Double) >>= print -- A random `Int` between 1 and 6 (A die roll) randomRIO (1, 6) >>= print
This is pretty much the interface that most other languages start with. Better would be to separate out IO as much as possible from the inevitable rest of our program. We can do that by confining IO usage to one operation: coming up with an initial RNG.
import System.Random dieRoll :: RandomGen g => g -> (Int, g) dieRoll = randomR (1, 6) main :: IO () main = do -- New generator, generated from the system RNG gen <- newStdGen let (result, newGen) = dieRoll gen print result let (newResult, newNewGen) = dieRoll newGen print newResult
We don’t want to duplicate this code every time we want to add a new die roll. The next logical step would be to sprinkle in some
State to store the current RNG in:
import System.Random import Control.Monad.State dieRoll :: RandomGen g => State g Int dieRoll = state (randomR (1, 6)) twoDice :: RandomGen g => State g Int twoDice = (+) <$> dieRoll <*> dieRoll main :: IO () main = do gen <- newStdGen print (evalState twoDice gen)
Now we can run more complex programs that employ random numbers. Note that
newStdGen can be replaced with
mkStdGen :: Int -> StdGen if you want to provide an integral seed instead of using the global
You can avoid some of the state boilerplate and get a few more benefits by bringing in the
MonadRandom package. Here’s some code that accomplishes the same goal using
import System.Random import Control.Monad.Random dieRoll :: RandomGen g => Rand g Int dieRoll = getRandomR (1, 6) twoDice :: RandomGen g => Rand g Int twoDice = (+) <$> dieRoll <*> dieRoll main :: IO () main = do gen <- newStdGen print (evalRand twoDice gen)
Apart from providing a nice way to write (slightly) terser randomness code,
MonadRandom is more explicit about the domain we’re working in, and ships with a couple of killer utilities; namely, the minimalistic sampling functions
MonadRandom 0.5). This program, for example, generates a list of 20 moves that might come up in a Dance Dance Revolution song:
import Control.Monad import Control.Monad.Random import System.Random data Direction = U | D | L | R deriving (Show, Eq) step :: RandomGen g => Rand g Direction step = uniform [U,D,L,R] stepWeighted :: RandomGen g => Rand g Direction stepWeighted = fromList [(U, 1), (D, 1), (L, 50), (R, 100)] danceDanceRevolutionScroll :: RandomGen g => Rand g [Direction] danceDanceRevolutionScroll = replicateM 20 $ do weightIt <- uniform [True, False] if weightIt then stepWeighted else step main :: IO () main = do gen <- newStdGen print (evalRand danceDanceRevolutionScroll gen)
fromList lets you specify weights for your random elements.
R will probably show up a lot more than the other two directions when this is run.
MonadRandom supplies some other conveniences as well, but it’s not crazy stuffed with functionality. It’s a nice package that contains the minimal amount of code to be useful but not overengineered.
That said, sometimes you need more. First off, what about different distributions? The normal distribution is a pretty common necessity.
random-fu really shines in this domain. You’ll have to pull in the
rvar package as well to run this next example, which will print out a random number pulled from a normal distribution with mean
100 and a standard deviation of
import Control.Monad import System.Random import Data.Random import Data.RVar import Control.Monad.State normalNumber :: State StdGen Double normalNumber = sampleRVar (normal 100 5) main :: IO () main = do gen <- newStdGen print (evalState normalNumber gen)
State pattern from earlier. Also, there a bunch of common distributions that ship with
One last thing I should mention is that we’re not tied to
StdGen, the RNG that ships with
In fact, it does not have strong statistical properties, and should probably be avoided for many “real” applications (See this reddit post, and thank you to reddit user tom-md for the note!).
There are faster and more stable ones ones, like
tf-random. These are both instances of
RandomGen, so you can plug either one of those in wherever you saw the generic type signature
RandomGen g => ... in this post. For example, mixing
PureMT back into
import Control.Monad.Random import Data.Random.Source.PureMT dieRoll :: RandomGen g => Rand g Int dieRoll = getRandomR (1, 6) main :: IO () main = newPureMT >>= print . evalRand dieRoll