Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can a pure function do IO?

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?

like image 993
Honza Pokorny Avatar asked Apr 30 '14 12:04

Honza Pokorny


1 Answers

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.

like image 123
bheklilr Avatar answered Oct 22 '22 03:10

bheklilr