我认为在抽象生成器的单子中进行需要随机数的计算是最干净的事情。这是该代码的样子:
我们将把 StdGen 实例放在一个状态单子中,然后在状态单子的 get 和 set 方法上提供一些糖来给我们随机数。
首先,加载模块:
import Control.Monad.State (State, evalState, get, put)
import System.Random (StdGen, mkStdGen, random)
import Control.Applicative ((<$>))
(通常我可能不会指定导入,但这很容易理解每个函数的来源。)
然后我们将定义我们的“需要随机数的计算”单子;在这种情况下,为State StdGen
called的别名R
。(因为“随机”和“兰德”已经有别的意思了。)
type R a = State StdGen a
R 的工作方式是:定义一个需要随机数流的计算(一元“动作”),然后使用以下命令“运行它” runRandom
:
runRandom :: R a -> Int -> a
runRandom action seed = evalState action $ mkStdGen seed
这需要一个动作和一个种子,并返回动作的结果。就像通常evalState
的 ,runReader
等一样。
现在我们只需要State monad周围的糖。我们用于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
它会起作用。