一个纯函数如何做IO?

Hon*_*rny 6 haskell functional-programming

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

getRandomR :: (MonadRandom m, Random a) => (a, a) -> m a
Run Code Online (Sandbox Code Playgroud)

显然,您可以编写一个函数,该函数使用getRandomR哪种类型的签名不包含任何内容IO.

computeSomething :: MonadRandom m => Int -> m Int
computeSomething a = getRandomR (0, a)
Run Code Online (Sandbox Code Playgroud)

根据调用者的不同,m将填写实例.如果它是从IO上下文运行的,那么该函数将是不纯的.

那么,问题是:一个不声称做的功能怎么能IO真正做到IO呢?如何判断这个computeSomething函数是纯粹的还是不纯的?

bhe*_*ilr 14

该功能getRandomR没有IO.有种子后,不需要IO生成随机数.该单子中以种子初始化,即可以是一个,你提供了使用从IO拉测试目的或一个.该单子可以做到这一点而不执行通过利用暴露在纯函数的动作从所述包,如和.这些函数中的每一个都使用生成器并返回新生成器和所需类型的随机值.在内部,Monad真的只是Monad,它的状态就是发电机.RandMonadRandomevalRandIORandIOSystem.RandomrandomrandomrandomRgRandStateg

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

所以回答你的问题

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

对于这个定义computeSomething,在MonadRandom解析实例之前,它既不纯粹也不纯净.如果我们将"纯粹"视为"不是IO"而"不纯"为"IO"(这不是完全准确,而是近似),那么computeSomething在某些情况下可能是纯粹的而在其他情况下则是不纯的,就像函数一样liftM2 :: Monad m => (a1 -> a2 -> r) -> m a1 -> m a2 -> m r可以在IOMonad Maybe[]Monads上使用.换一种说法:

liftM2 (+) (Just 1) (Just 2)
Run Code Online (Sandbox Code Playgroud)

将永远返回Just 3,所以它可以被认为是纯粹的,而

liftM2 (++) getLine getLine
Run Code Online (Sandbox Code Playgroud)

不会总是返回相同的东西.虽然每个预定义的实例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
Run Code Online (Sandbox Code Playgroud)

所以当我们有一个功能

computeSomething  :: MonadRandom m => Int -> m Int
computeSomething high = getRandomR (0, high)
Run Code Online (Sandbox Code Playgroud)

然后我们可以用它作为

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

要么

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
Run Code Online (Sandbox Code Playgroud)

或者,如果您想使用已知的生成器进行测试:

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

在最后一种情况下,它每次都会打印相同的数字,使"随机"过程具有确定性和纯粹性.这使您能够通过提供显式种子来重新运行生成随机数的实验,或者您可以传入系统的随机生成器一次,或者您可以直接IO使用每个调用获得一个新的随机生成器.所有这一切都是可能的,而不必更改一行代码而不是它的调用方式main,computeSomething这三种用法之间的定义不会改变.