Sono passati 7 anni da quando è stata posta questa domanda e sembra che nessuno abbia ancora trovato una buona soluzione a questo problema. Repa non ha una funzione mapM/ traverselike, anche una che potrebbe essere eseguita senza parallelizzazione. Inoltre, considerando l'entità dei progressi compiuti negli ultimi anni, sembra improbabile che ciò accada.
A causa dello stato obsoleto di molte librerie di array in Haskell e della mia insoddisfazione generale per i loro set di funzionalità, ho messo avanti un paio di anni di lavoro in una libreria di array massiv, che prende in prestito alcuni concetti da Repa, ma lo porta a un livello completamente diverso. Basta con l'intro.
Prima di oggi, c'era tre mappa monadico come funzioni massiv(non contando il sinonimo come funzioni: imapM, forMet al.):
mapM- la solita mappatura in modo arbitrario Monad. Non parallelizzabile per ovvi motivi ed è anche un po 'lento (sulla falsariga del solito mapMsu un elenco lento)
traversePrim- qui siamo limitati a PrimMonad, che è significativamente più veloce di mapM, ma la ragione di ciò non è importante per questa discussione.
mapIO- questo, come suggerisce il nome, è limitato a IO(o meglio MonadUnliftIO, ma è irrilevante). Poiché ci IOtroviamo, possiamo dividere automaticamente l'array in tanti blocchi quanti sono i core e utilizzare thread di lavoro separati per mappare l' IOazione su ogni elemento in quei blocchi. A differenza di Pure fmap, che è anche parallelizzabile, dobbiamo essere IOqui a causa del non determinismo della pianificazione combinato con gli effetti collaterali della nostra azione di mappatura.
Quindi, una volta che ho letto questa domanda, ho pensato a me stesso che il problema è praticamente risolto massiv, ma non così velocemente. I generatori di numeri casuali, come in mwc-randome altri in random-funon possono utilizzare lo stesso generatore su molti thread. Il che significa che l'unico pezzo del puzzle che mi mancava era: "disegnare un nuovo seme casuale per ogni thread generato e procedere come al solito". In altre parole, avevo bisogno di due cose:
- Una funzione che inizializzerebbe tanti generatori quanti sono i thread di lavoro
- e un'astrazione che fornirebbe perfettamente il generatore corretto alla funzione di mappatura a seconda del thread in cui è in esecuzione l'azione.
Quindi è esattamente quello che ho fatto.
Per prima cosa fornirò esempi usando le funzioni randomArrayWSe appositamente predisposte initWorkerStates, poiché sono più rilevanti per la domanda e successivamente passerò alla mappa monadica più generale. Ecco le loro firme di tipo:
randomArrayWS ::
(Mutable r ix e, MonadUnliftIO m, PrimMonad m)
=> WorkerStates g -- ^ Use `initWorkerStates` to initialize you per thread generators
-> Sz ix -- ^ Resulting size of the array
-> (g -> m e) -- ^ Generate the value using the per thread generator.
-> m (Array r ix e)
initWorkerStates :: MonadIO m => Comp -> (WorkerId -> m s) -> m (WorkerStates s)
Per coloro che non hanno familiarità con massiv, l' Compargomento è una strategia di calcolo da utilizzare, i costruttori notevoli sono:
Seq - Esegui il calcolo in modo sequenziale, senza forkare alcun thread
Par - avvia tanti thread quante sono le capacità e usali per fare il lavoro.
mwc-randomInizialmente userò il pacchetto come esempio e successivamente passerò a RVarT:
λ> import Data.Massiv.Array
λ> import System.Random.MWC (createSystemRandom, uniformR)
λ> import System.Random.MWC.Distributions (standard)
λ> gens <- initWorkerStates Par (\_ -> createSystemRandom)
Sopra abbiamo inizializzato un generatore separato per thread usando la casualità del sistema, ma avremmo potuto usare altrettanto bene un seme univoco per thread derivandolo WorkerIddall'argomento, che è un semplice Intindice del worker. E ora possiamo usare quei generatori per creare un array con valori casuali:
λ> randomArrayWS gens (Sz2 2 3) standard :: IO (Array P Ix2 Double)
Array P Par (Sz (2 :. 3))
[ [ -0.9066144845415213, 0.5264323240310042, -1.320943607597422 ]
, [ -0.6837929005619592, -0.3041255565826211, 6.53353089112833e-2 ]
]
Usando la Parstrategia la schedulerlibreria dividerà equamente il lavoro di generazione tra i lavoratori disponibili e ogni lavoratore utilizzerà il proprio generatore, rendendolo così thread-safe. Niente ci impedisce di riutilizzare lo stesso WorkerStatesnumero arbitrario di volte fintanto che non viene eseguito contemporaneamente, il che altrimenti risulterebbe in un'eccezione:
λ> randomArrayWS gens (Sz1 10) (uniformR (0, 9)) :: IO (Array P Ix1 Int)
Array P Par (Sz1 10)
[ 3, 6, 1, 2, 1, 7, 6, 0, 8, 8 ]
Ora mettendo mwc-randomda parte possiamo riutilizzare lo stesso concetto per altri possibili casi d'uso utilizzando funzioni come generateArrayWS:
generateArrayWS ::
(Mutable r ix e, MonadUnliftIO m, PrimMonad m)
=> WorkerStates s
-> Sz ix -- ^ size of new array
-> (ix -> s -> m e) -- ^ element generating action
-> m (Array r ix e)
e mapWS:
mapWS ::
(Source r' ix a, Mutable r ix b, MonadUnliftIO m, PrimMonad m)
=> WorkerStates s
-> (a -> s -> m b) -- ^ Mapping action
-> Array r' ix a -- ^ Source array
-> m (Array r ix b)
Ecco l'esempio promesso su come utilizzare questa funzionalità con rvar, random-fue mersenne-random-pure64le librerie. Avremmo potuto usarlo anche randomArrayWSqui, ma per motivi di esempio diciamo che abbiamo già un array con RVarTs differenti , nel qual caso abbiamo bisogno di mapWS:
λ> import Data.Massiv.Array
λ> import Control.Scheduler (WorkerId(..), initWorkerStates)
λ> import Data.IORef
λ> import System.Random.Mersenne.Pure64 as MT
λ> import Data.RVar as RVar
λ> import Data.Random as Fu
λ> rvarArray = makeArrayR D Par (Sz2 3 9) (\ (i :. j) -> Fu.uniformT i j)
λ> mtState <- initWorkerStates Par (newIORef . MT.pureMT . fromIntegral . getWorkerId)
λ> mapWS mtState RVar.runRVarT rvarArray :: IO (Array P Ix2 Int)
Array P Par (Sz (3 :. 9))
[ [ 0, 1, 2, 2, 2, 4, 5, 0, 3 ]
, [ 1, 1, 1, 2, 3, 2, 6, 6, 2 ]
, [ 0, 1, 2, 3, 4, 4, 6, 7, 7 ]
]
È importante notare che, nonostante il fatto che nell'esempio precedente venga utilizzata la pura implementazione di Mersenne Twister, non possiamo sfuggire all'IO. Ciò è dovuto alla pianificazione non deterministica, il che significa che non sappiamo mai quale dei worker gestirà quale blocco dell'array e di conseguenza quale generatore verrà utilizzato per quale parte dell'array. D'altro canto, se il generatore è puro e divisibile, come splitmix, allora possiamo usare la funzione di generazione pura, deterministica e parallelizzabile :, randomArrayma questa è già una storia a parte.