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
computeSomethingfunction 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