2014-04-30 23 views
5

我最近了解到MonadRandom库。它给你一个名为getRandomR功能和它的类型签名是:纯函数如何做IO?

getRandomR :: (MonadRandom m, Random a) => (a, a) -> m a 

显然,您可以编写一个使用getRandomR谁类型签名不包含任何IO的功能。

computeSomething :: MonadRandom m => Int -> m Int 
computeSomething a = getRandomR (0, a) 

根据调用者,m实例将被填写。如果从IO上下文运行,该功能将不纯。

所以,这个问题:一个函数如何不要求做IO实际上做IO?如何判断这个computeSomething函数是纯粹还是不纯?

+0

这是一个'class'没有实现。你可以实例化一些纯粹的“随机”生成器(例如固定种子,通过arg,...)。最后的'm' monad将决定最终的纯度。 – josejuan

+1

实际上,getRandomR的类型是(MonadRandom米,随机A)=>(A,A) - > m是一。 –

+1

@HonzaPokorny我已经添加了一些编辑到我的答案,可能会更好的为您的具体问题。 – bheklilr

回答

14

功能getRandomR没有做IO。它不要求做IO生成随机数一旦你有一个种子。该Rand单子在MonadRandom以种子初始化,即可以是一个您提供用于测试目的或一个从IO使用evalRandIO拉。 Rand Monad可以在不执行IO操作的情况下执行此操作,方法是利用random程序包的System.Random中显示的纯函数,例如randomrandomR。这些函数中的每一个都带有一个生成器g并返回一个新的生成器和一个所需类型的随机值。在内部,Rand单子其实只是State单子,而它的状态是发电机g

然而,值得注意的是,IO单子是MonadRandom,它不使用纯态的功能,它使用正常IO功能,如randomIO一个实例是很重要的。您可以使用IORand互换,但后者会多一点效率的(不必每次执行系统调用),你可以用于测试目的以获得可重复的结果已知值的种子吧。

因此,要回答你的问题

一个如何判断这个computeSomething功能将是纯或不纯?

对于这个定义的computeSomething,这既不是纯或不纯直到MonadRandom实例解析。如果我们把“纯粹”是“不IO”和“不纯”是“IO”(which is not entirely accurate, but a close approximation),然后computeSomething可以在其他一些情况下,与不纯的纯洁,就像函数liftM2 :: Monad m => (a1 -> a2 -> r) -> m a1 -> m a2 -> m r可以在IO单子使用或在Maybe[] Monads上。换句话说:

liftM2 (+) (Just 1) (Just 2) 

总是会返回Just 3,所以可以考虑,而

liftM2 (++) getLine getLine 

不会总是返回相同的事情。虽然MonadRandom的每个预定义实例都被认为是不纯的(RandTRand有内部状态,所以它们在技术上不纯),但您可以提供自己的数据类型,其实例为MonadRandom,当getRandom或其他MonadRandom函数被调用。出于这个原因,我会说MonadRandom不是固有的纯粹或不纯净的。


也许一些代码将有助于解释它(简体,我跳过RandT变压器):

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 

所以,当我们有一个函数

computeSomething :: MonadRandom m => Int -> m Int 
computeSomething high = getRandomR (0, high) 

然后我们可以使用它作为

main :: IO() 
main = do 
    i <- computeSomething 10 
    putStrLn $ "A random number between 0 and 10: " ++ show i 

或者

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 

或者,如果你想使用一个已知的发电机进行测试:

main :: IO() 
main = do 
    let myGen = R.mkStdGen 12345 
     i = evalRand (computeSomething 10) myGen 
    putStrLn $ "A random number between 0 and 10: " ++ show i 

在后一种情况下,它会打印相同数量的每一次,做一个“随机”的过程确定性和纯粹。这使您可以通过提供其明确的种子重新运行实验证明,产生随机数的能力,也可以在系统中的随机数发生器通过一次,也可以使用直IO获得每次调用一个新的随机数生成器。所有这一切都是可能的,而无需改变线路的比它是如何调用main其他代码的computeSomething的定义并没有这3种用途之间进行切换。