一个纯函数如何做IO?

时间:2014-04-30 12:27:35

标签: haskell functional-programming

我最近了解了MonadRandom库。它为您提供了一个名为getRandomR的函数,其类型签名为:

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函数是纯粹的还是不纯的?

1 个答案:

答案 0 :(得分:14)

函数getRandomR没有IO。一旦有种子,就不需要IO生成随机数Rand中的MonadRandom Monad使用种子进行初始化,该种子可以是您为测试目的提供的种子,也可以是使用evalRandIO从IO中提取的种子。 Rand Monad可以通过利用IO包中的System.Random中公开的纯函数(例如random和{{1})执行random操作,从而执行此操作}}。这些函数中的每一个都使用生成器randomR并返回一个新生成器和所需类型的随机值。在内部,g Monad实际上只是Rand Monad,其状态是生成器State

但是,重要的是要注意g Monad是IO的一个实例,而不是使用纯状态函数,它使用正常的MonadRandom函数,如{{ 1}}。您可以互换使用IOrandomIO,但后者效率会更高(每次都不必执行系统调用),您可以使用已知值为其播种测试目的是为了获得可重复的结果。

所以回答你的问题

  

如何判断这个IO函数是纯粹的还是不纯的?

对于Rand的这个定义,在解析computeSomething的实例之前,它既不纯粹也不纯洁。如果我们采取"纯"是"不是IO"并且"不纯的"成为" IO" (which is not entirely accurate, but a close approximation),然后computeSomething在某些情况下可能是纯粹的,而在其他情况下则不纯,就像函数MonadRandom可以在computeSomething Monad或{{liftM2 :: Monad m => (a1 -> a2 -> r) -> m a1 -> m a2 -> m r上使用一样1}}或IO Monads。换句话说:

Maybe

将始终返回[],因此可以将其视为,而

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

不会总是返回相同的东西。虽然Just 3的每个预定义实例都被认为是不纯的(liftM2 (++) getLine getLine MonadRandom具有内部状态,因此它们在技术上是不纯的),您可以使用实例提供自己的数据类型在调用RandT或其他Rand函数时始终返回相同值的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

在最后一种情况下,它每次都会打印相同的数字,并随机生成一个&#34;随机的&#34;过程确定性和纯粹。这使您能够重新运行通过提供显式种子来生成随机数的实验,或者您可以传入系统的随机生成器一次,或者您可以使用直接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的定义在这3个用途之间不会发生变化。