Minęło 7 lat, odkąd zadano to pytanie, a nadal wydaje się, że nikt nie znalazł dobrego rozwiązania tego problemu. Repa nie ma funkcji podobnej do mapM
/ traverse
, nawet takiej, która mogłaby działać bez równoległości. Co więcej, biorąc pod uwagę postęp, jaki dokonał się w ciągu ostatnich kilku lat, wydaje się mało prawdopodobne, aby tak się stało.
Ze względu na przestarzały stan wielu bibliotek tablicowych w Haskell i moje ogólne niezadowolenie z ich zestawów funkcji, włożyłem kilka lat pracy nad biblioteką tablicową massiv
, która pożycza niektóre koncepcje od Repa, ale przenosi ją na zupełnie inny poziom. Dość z intro.
Przed dzisiaj, nie było trzy monadycznego mapa jak funkcje w massiv
(nie licząc synonim jak funkcje: imapM
, forM
et al.):
mapM
- zwykłe mapowanie w sposób arbitralny Monad
. Nie można zrównoleglać z oczywistych powodów, a także jest nieco powolny (podobnie jak zwykle mapM
na liście wolno)
traversePrim
- tutaj jesteśmy ograniczeni PrimMonad
, co jest znacznie szybsze niż mapM
, ale przyczyna tego nie jest istotna dla tej dyskusji.
mapIO
- ten, jak nazwa sugeruje, ogranicza się do IO
(a raczej MonadUnliftIO
, ale to nie ma znaczenia). Ponieważ jesteśmy w IO
środku, możemy automatycznie podzielić tablicę na tyle fragmentów, ile jest rdzeni, i użyć oddzielnych wątków roboczych, aby zmapować IO
działanie każdego elementu w tych fragmentach. W przeciwieństwie do pure fmap
, który jest również równoległy, musimy być IO
tutaj z powodu niedeterminizmu planowania w połączeniu z efektami ubocznymi naszej akcji mapowania.
Kiedy więc przeczytałem to pytanie, pomyślałem, że problem został praktycznie rozwiązany massiv
, ale nie tak szybko. Generatory liczb losowych, takie jak in mwc-random
i inne w, random-fu
nie mogą używać tego samego generatora w wielu wątkach. Co oznacza, że jedynym elementem układanki, którego mi brakowało, było: „losowanie nowego, losowego nasienia dla każdej zrodzonej nitki i kontynuowanie normalnego postępowania”. Innymi słowy, potrzebowałem dwóch rzeczy:
- Funkcja, która zainicjuje tyle generatorów, ile będzie wątków roboczych
- oraz abstrakcja, która bezproblemowo zapewni prawidłowy generator funkcji mapującej w zależności od wątku, w którym działa akcja.
Więc to jest dokładnie to, co zrobiłem.
Najpierw podam przykłady przy użyciu specjalnie spreparowanych funkcji randomArrayWS
i initWorkerStates
funkcji, ponieważ są one bardziej odpowiednie dla pytania, a później przejdę do bardziej ogólnej mapy monadycznej. Oto ich podpisy typu:
randomArrayWS ::
(Mutable r ix e, MonadUnliftIO m, PrimMonad m)
=> WorkerStates g
-> Sz ix
-> (g -> m e)
-> m (Array r ix e)
initWorkerStates :: MonadIO m => Comp -> (WorkerId -> m s) -> m (WorkerStates s)
Dla tych, którzy nie są zaznajomieni massiv
, Comp
argumentem jest strategia obliczeniowa, godnymi uwagi konstruktorami są:
Seq
- uruchamiaj obliczenia sekwencyjnie, bez rozwidlania żadnych wątków
Par
- rozkręć tyle wątków, ile jest możliwości i wykorzystaj je do wykonania pracy.
Na początku użyję mwc-random
pakietu jako przykładu, a później przejdę do RVarT
:
λ> import Data.Massiv.Array
λ> import System.Random.MWC (createSystemRandom, uniformR)
λ> import System.Random.MWC.Distributions (standard)
λ> gens <- initWorkerStates Par (\_ -> createSystemRandom)
Powyżej zainicjowaliśmy oddzielny generator na wątek przy użyciu losowości systemowej, ale równie dobrze moglibyśmy użyć unikalnego ziarna dla każdego wątku, wyprowadzając je z WorkerId
argumentu, który jest zwykłym Int
indeksem procesu roboczego. A teraz możemy użyć tych generatorów do stworzenia tablicy z losowymi wartościami:
λ> 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 ]
]
Korzystając ze Par
strategii, scheduler
biblioteka podzieli równomiernie pracę pokolenia na dostępnych pracowników, a każdy pracownik będzie korzystał z własnego generatora, dzięki czemu będzie bezpieczny dla wątków. Nic nie stoi na przeszkodzie, abyśmy ponownie wykorzystali tę samą WorkerStates
dowolną liczbę razy, o ile nie jest to wykonywane jednocześnie, co w przeciwnym razie skutkowałoby wyjątkiem:
λ> 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 ]
Teraz odkładając mwc-random
na bok, możemy ponownie wykorzystać tę samą koncepcję do innych możliwych zastosowań, używając funkcji takich jak generateArrayWS
:
generateArrayWS ::
(Mutable r ix e, MonadUnliftIO m, PrimMonad m)
=> WorkerStates s
-> Sz ix
-> (ix -> s -> m e)
-> m (Array r ix e)
i mapWS
:
mapWS ::
(Source r' ix a, Mutable r ix b, MonadUnliftIO m, PrimMonad m)
=> WorkerStates s
-> (a -> s -> m b)
-> Array r' ix a
-> m (Array r ix b)
Oto obiecany przykład, w jaki sposób korzystać z tej funkcjonalności z rvar
, random-fu
i mersenne-random-pure64
bibliotek. Mogliśmy również użyć randomArrayWS
tutaj, ale dla przykładu powiedzmy, że mamy już tablicę z różnymi RVarT
s, w takim przypadku potrzebujemy 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 ]
]
Należy zauważyć, że pomimo faktu, że w powyższym przykładzie używana jest czysta implementacja Mersenne Twister, nie możemy uciec od IO. Dzieje się tak z powodu niedeterministycznego planowania, co oznacza, że nigdy nie wiemy, który z procesów roboczych będzie obsługiwał który fragment tablicy, a co za tym idzie, który generator będzie używany dla której części tablicy. Z drugiej strony, jeśli generator jest czysty i podzielny, na przykład splitmix
, możemy użyć czystej, deterministycznej i równoległej funkcji generującej: randomArray
ale to już jest osobna historia.