2014-04-30 68 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種用途之間進行切換。