I recently learned about the MonadRandom library. It gives you a function called getRandomR
and its type signature is:
getRandomR :: (MonadRandom m, Random a) => (a, a) -> m a
Apparently, you can write a function that uses getRandomR
who type signature doesn't contain anything about IO
.
computeSomething :: MonadRandom m => Int -> m Int
computeSomething a = getRandomR (0, a)
Depending on the caller, the m
instance will be filled out. If it's run from an IO
context, the function will be impure.
So, the question: how can a function that doesn't claim to do IO
actually do IO
? How can one tell if this computeSomething
function will be pure or impure?
The function getRandomR
is not doing IO
. It is not required to do IO
to generate random numbers once you have a seed. The Rand
Monad in MonadRandom
is initialized with a seed, that can either be one that you provide for testing purposes or one pulled from IO using evalRandIO
. The Rand
Monad can do this without performing IO
actions by leveraging the pure functions exposed in System.Random
from the random
package, such as random
and randomR
. Each of these functions takes a generator g
and returns a new generator and a random value of the desired type. Internally, the Rand
Monad is really just the State
Monad, and its state is the generator g
.
However, it is important to note that the IO
Monad is an instance of MonadRandom
, where instead of using the pure state functions, it uses the normal IO
functions like randomIO
. You can use IO
and Rand
interchangeably, but the latter will be a bit more efficient (doesn't have to perform a system call each time) and you can seed it with a known value for testing purposes to get repeatable results.
So to answer your question
How can one tell if this
computeSomething
function will be pure or impure?
For this definition of computeSomething
, it is neither pure or impure until the instance for MonadRandom
is resolved. If we take "pure" to be "not IO" and "impure" to be "IO" (which is not entirely accurate, but a close approximation), then computeSomething
can be pure in some instances and impure in others, just as the function liftM2 :: Monad m => (a1 -> a2 -> r) -> m a1 -> m a2 -> m r
can be used on the IO
Monad or on the Maybe
or []
Monads. In other words:
liftM2 (+) (Just 1) (Just 2)
Will always return Just 3
, so it can be considered pure, while
liftM2 (++) getLine getLine
Will not always return the same thing. While each predefined instance for MonadRandom
would be considered impure (RandT
and Rand
have internal state, so they're technically impure), you could provide your own data type with an instance of MonadRandom
for which it always returns the same value when getRandom
or the other MonadRandom
functions are called. For this reason, I would say that MonadRandom
is not inherently pure or impure.
Maybe some code will help explain it (simplified, I'm skipping the RandT
transformer):
import Control.Monad.State
import qualified System.Random as R
class MonadRandom m where
getRandom :: Random a => m a
getRandoms :: Random a => m [a]
getRandomR :: Random a => (a, a) -> m a
getRandomRs :: Random a => (a, a) -> m [a]
-- Not the real definition, the MonadRandom library defines a RandT
-- Monad transformer where Rand g a = RandT g Identity a, with
-- newtype RandT g m a = RandT (StateT g m a), but I'm trying to
-- keep things simple for this example.
newtype Rand g a = Rand { unRand :: State g a }
instance Monad (Rand g) where
-- Implementation isn't relevant here
instance RandomGen g => MonadRandom (Rand g) where
getRandom = state R.random
getRandoms = sequence $ repeat getRandom
getRandomR range = state (R.randomR range)
getRandomRs range = sequence $ repeat $ getRandomR range
instance MonadRandom IO where
getRandom = R.randomIO
getRandoms = sequence $ repeat getRandom
getRandomR range = R.randomRIO range
getRandomRs range = sequence $ repeat $ getRandomR range
So when we have a function
computeSomething :: MonadRandom m => Int -> m Int
computeSomething high = getRandomR (0, high)
Then we can use it as
main :: IO ()
main = do
i <- computeSomething 10
putStrLn $ "A random number between 0 and 10: " ++ show i
Or
main :: IO ()
main = do
-- evalRandIO uses getStdGen and passes the generator in for you
i <- evalRandIO $ computeSomething 10
putStrLn $ "A random number between 0 and 10: " ++ show i
Or if you wanted to use a known generator to test with:
main :: IO ()
main = do
let myGen = R.mkStdGen 12345
i = evalRand (computeSomething 10) myGen
putStrLn $ "A random number between 0 and 10: " ++ show i
In the last case, it will print the same number every time, making a "random" process deterministic and pure. This gives you the ability to re-run experiments that generate random numbers by providing it an explicit seed, or you can pass in the system's random generator once, or you can use straight IO
to get a new random generator with each call. All of this is possible without having to change a line of code other than how it's called in main
, the definition of computeSomething
doesn't change between those 3 uses.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With