2014-04-30 16 views
5

Recentemente ho imparato a conoscere la libreria MonadRandom. Ti dà una funzione chiamata getRandomR e la sua firma tipo è:Come può una funzione pura fare IO?

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

A quanto pare, è possibile scrivere una funzione che utilizza getRandomR che tipo di firma non contiene nulla di IO.

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

A seconda del chiamante, l'istanza m saranno riempiti fuori. Se viene eseguito da un contesto IO, la funzione sarà impura.

Quindi, la domanda: come può una funzione che non pretende di fare IO effettivamente fare IO? Come si può sapere se questa funzione computeSomething sarà pura o impura?

+0

È un 'class' non un'implementazione. È possibile ad esempio un puro generatore "casuale" (ad esempio, seme fisso, arg, ...). La finale 'm' monad deciderà la purezza finale. – josejuan

+1

In realtà, il tipo di getRandomR è (MonadRandom m, Random a) => (a, a) -> m a. –

+1

@HonzaPokorny Ho aggiunto alcune modifiche alla mia risposta che potrebbero essere migliori per la tua domanda specifica. – bheklilr

risposta

14

La funzione getRandomR non funziona IO. Non è necessario eseguire IO per generare numeri casuali dopo aver ottenuto un seme. Il Rand Monad in MonadRandom viene inizializzato con un seme, che può essere uno fornito per scopi di test o uno estratto da IO utilizzando evalRandIO. La Monade Rand può eseguire questa operazione senza eseguire le azioni IO sfruttando le funzioni pure esposte in System.Random dal pacchetto random, ad esempio random e randomR. Ognuna di queste funzioni richiede un generatore g e restituisce un nuovo generatore e un valore casuale del tipo desiderato. Internamente, la Monade Rand è in realtà solo la Monade State e il suo stato è il generatore g.

Tuttavia, è importante notare che il IO Monade è un'istanza di MonadRandom, dove invece di usare le funzioni di stato puro, utilizza le normali funzioni come IOrandomIO. È possibile utilizzare IO e Rand in modo intercambiabile, ma quest'ultimo sarà un po 'più efficiente (non è necessario eseguire una chiamata di sistema ogni volta) e sarà possibile inizializzarlo con un valore noto a fini di test per ottenere risultati ripetibili.

Quindi, per rispondere alla tua domanda

Come si può dire se questa funzione computeSomething sarà pura o impura?

Per questa definizione di computeSomething, non è né pura o impura fino alla risoluzione del istanza per MonadRandom. Se prendiamo "puro" per essere "non IO" e "impuro" per essere "IO" (which is not entirely accurate, but a close approximation), quindi computeSomething può essere puro in alcuni casi e impuro in altri, proprio come la funzione liftM2 :: Monad m => (a1 -> a2 -> r) -> m a1 -> m a2 -> m r può essere utilizzata sul IO Monade o sulle Monete Maybe o []. In altre parole:

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

restituirà sempre Just 3, in modo che possa essere considerato puro, mentre

liftM2 (++) getLine getLine 

non tornerà sempre la stessa cosa.Mentre ogni istanza predefinita per MonadRandom viene considerata impura (RandT e hanno lo stato interno, quindi sono tecnicamente impure), è possibile fornire il proprio tipo di dati con un'istanza di MonadRandom per la quale restituisce sempre lo stesso valore quando getRandom o il valore vengono chiamate altre funzioni MonadRandom. Per questo motivo, direi che MonadRandom non è intrinsecamente puro o impuro.


Forse un po 'di codice contribuirà a spiegare (semplificato, sto saltando il RandT trasformatore):

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 

Così, quando abbiamo una funzione

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

Poi possiamo usare it

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

O

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 

O se si voleva utilizzare un generatore di nota per testare con:

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 quest'ultimo caso, verrà stampato lo stesso numero ogni volta, facendo un processo di "random" deterministico e puro. Questo ti dà la possibilità di ri-eseguire esperimenti che generano numeri casuali fornendo un seme esplicito, oppure puoi passare nel generatore casuale del sistema una volta, oppure puoi usare il IO per ottenere un nuovo generatore casuale con ogni chiamata. Tutto ciò è possibile senza dover modificare una riga di codice diversa da come viene chiamata in main, la definizione di computeSomething non cambia tra questi 3 usi.

Problemi correlati