How to avoid the IO
depends on why it's being introduced in the first place. While pseudo-random number generators are inherently state-oriented, there's no reason IO
needs to be involved.
I'm going to take a guess and say that you're using newStdGen
or getStdGen
to get your initial PRNG. If that's the case, then there's no way to completely escape IO
. You could instead seed the PRNG directly with mkStdGen
, keeping in mind that the same seed will result in the same "random" number sequence.
More likely, what you want to do is get a PRNG inside IO
, then pass that as an argument to a pure function. The entire thing will still be wrapped in IO
at the end, of course, but the intermediate computations won't need it. Here's a quick example to give you the idea:
import System.Random
type Rand a = StdGen -> (a, StdGen)
getPRNG = do
rng <- newStdGen
let x = usePRNG rng
print x
usePRNG :: StdGen -> [[Int]]
usePRNG rng = let (x, rng') = randomInts 5 rng
(y, _) = randomInts 10 rng'
in [x, y]
randomInts :: Int -> Rand [Int]
randomInts 0 rng = ([], rng)
randomInts n rng = let (x, rng') = next rng
(xs, rng'') = randomInts (n - 1) rng'
in (x:xs, rng'')
You might notice that the code using the PRNG gets pretty ugly due to passing the current value back and forth constantly. It's also potentially error prone, since it'd be easy to accidentally reuse an old value. As mentioned above, using the same PRNG value will give the same sequence of numbers, which is usually not what you want. Both problems are a perfect example of where it makes sense to use a State
monad--which is getting off topic here, but you may want to look into it next.