Я думаю, что выполнение ваших вычислений, которые требуют случайных чисел внутри монады, которая абстрагирует генератор, является самой чистой вещью. Вот как будет выглядеть этот код:
Мы собираемся поместить экземпляр StdGen в монаду состояний, а затем добавить сахар к методу get и set монады, чтобы получить случайные числа.
Сначала загрузите модули:
import Control.Monad.State (State, evalState, get, put)
import System.Random (StdGen, mkStdGen, random)
import Control.Applicative ((<$>))
(Обычно я, вероятно, не буду указывать импорт, но это позволяет легко понять, откуда поступает каждая функция.)
Затем мы определим монаду «вычисления, требующие случайных чисел»; в этом случае псевдоним для State StdGen
называется R
. (Потому что «Случайный» и «Рэнд» уже означают что-то другое.)
type R a = State StdGen a
Способ работы R таков: вы определяете вычисление, для которого требуется поток случайных чисел (монадическое «действие»), а затем «запускаете его» с помощью runRandom
:
runRandom :: R a -> Int -> a
runRandom action seed = evalState action $ mkStdGen seed
Это принимает действие и семя и возвращает результаты действия. Как обычно evalState
, runReader
и т. Д.
Теперь нам просто нужен сахар вокруг государственной монады. Мы используем get
, чтобы получить StdGen, и мы используем put
, чтобы установить новое состояние. Это означает, что для получения одного случайного числа мы должны написать:
rand :: R Double
rand = do
gen <- get
let (r, gen') = random gen
put gen'
return r
Мы получаем текущее состояние генератора случайных чисел, используем его для получения нового случайного числа и нового генератора, сохраняем случайное число, устанавливаем новое состояние генератора и возвращаем случайное число.
Это «действие», которое можно запустить с помощью runRandom, поэтому давайте попробуем:
ghci> runRandom rand 42
0.11040701265689151
it :: Double
Это чистая функция, поэтому, если вы запустите ее снова с теми же аргументами, вы получите тот же результат. Примесь остается внутри «действия», которое вы передаете runRandom.
В любом случае, вашей функции нужны пары случайных чисел, поэтому давайте напишем действие для получения пары случайных чисел:
randPair :: R (Double, Double)
randPair = do
x <- rand
y <- rand
return (x,y)
Запустите это с runRandom, и вы увидите два разных числа в паре, как и следовало ожидать. Но обратите внимание, что вам не нужно указывать аргумент «rand»; это потому, что функции чисты, но «rand» - это действие, которое не обязательно должно быть чистым.
Теперь мы можем реализовать вашу функцию normals
. boxMuller
как вы определили выше, я просто добавил сигнатуру типа, чтобы я мог понять, что происходит без необходимости читать всю функцию:
boxMuller :: Double -> Double -> (Double, Double) -> Double
boxMuller mu sigma (r1,r2) = mu + sigma * sqrt (-2 * log r1) * cos (2 * pi * r2)
Со всеми реализованными вспомогательными функциями / действиями мы можем наконец написать normals
, действие с 0 аргументами, которое возвращает (сгенерированный лениво) бесконечный список нормально распределенных двойников:
normals :: R [Double]
normals = mapM (\_ -> boxMuller 0 1 <$> randPair) $ repeat ()
Вы также можете написать это менее кратко, если хотите:
oneNormal :: R Double
oneNormal = do
pair <- randPair
return $ boxMuller 0 1 pair
normals :: R [Double]
normals = mapM (\_ -> oneNormal) $ repeat ()
repeat ()
дает монадическому действию бесконечный поток, в котором ничто не может вызвать функцию (и это делает результат нормалей бесконечно длинным). Сначала я написал [1..]
, но я переписал его, чтобы удалить бессмысленный 1
из текста программы. Мы не работаем с целыми числами, мы просто хотим бесконечный список.
Во всяком случае, все. Чтобы использовать это в реальной программе, вы просто выполняете свою работу, требуя случайных нормалей внутри действия R:
someNormals :: Int -> R [Double]
someNormals x = liftM (take x) normals
myAlgorithm :: R [Bool]
myAlgorithm = do
xs <- someNormals 10
ys <- someNormals 10
let xys = zip xs ys
return $ uncurry (<) <$> xys
runRandom myAlgorithm 42
Применяются обычные методы программирования монадических действий; сохраняйте как можно меньше своего приложения в R
, и все будет не так уж и грязно.
Да, и еще: лень может «просочиться» за границу монады чисто. Это означает, что вы можете написать:
take 10 $ runRandom normals 42
и это будет работать.