这是我以前的后续行动question
Travis Brown指出 java.util.Random
是副作用并且建议使用随机monad Rng
library来使代码纯粹起作用 . 现在我正在尝试自己构建一个简化的随机monad以了解它是如何工作的 .
是否有意义 ?您如何修复/改进下面的解释?
随机生成器
首先我们抄袭 java.util.Random
的随机生成函数
// do some bit magic to generate a new random "seed" from the given "seed"
// and return both the new "seed" and a random value based on it
def next(seed: Long, bits: Int): (Long, Int) = ...
请注意, next
返回新种子和值,而不仅仅返回值 . 我们需要它将新种子传递给另一个函数调用 .
随机点
现在让我们编写一个函数来生成单位平方的随机点 .
假设我们有一个函数来生成范围[0,1]中的随机双精度
def randomDouble(seed: Long): (Long, Double) = ... // some bit magic
现在我们可以编写一个函数来生成一个随机点 .
def randomPoint(seed: Long): (Long, (Double, Double)) = {
val (seed1, x) = randomDouble(seed)
val (seed2, y) = randomDouble(seed1)
(seed2, (x, y))
}
到目前为止,这么好,并且 randomDouble
和 randomPoint
都是纯粹的 . 唯一的问题是我们撰写 randomDouble
来构建 randomPoint
ad hoc . 我们没有一个通用的工具来组合产生随机值的函数 .
Monad随机
现在我们将定义一个通用工具来组合产生随机值的函数 . 首先,我们推广 randomDouble
的类型:
type Random[A] = Long => (Long, A) // generate a random value of type A
然后围绕它构建一个包装类 .
class Random[A](run: Long => (Long, A))
我们需要包装器来定义方法 flatMap
(在Haskell中作为bind)和for-comprehension使用的 map
.
class Random[A](run: Long => (Long, A)) {
def apply(seed: Long) = run(seed)
def flatMap[B](f: A => Random[B]): Random[B] =
new Random({seed: Long => val (seed1, a) = run(seed); f(a)(seed1)})
def map[B](f: A => B): Random[B] =
new Random({seed: Long = val (seed1, a) = run(seed); (seed1, f(a))})
}
现在我们添加一个工厂函数来创建一个简单的 Random[A]
(顺便说一句,这绝对是确定性的而不是"random")这是一个返回函数(作为Haskell中的返回) .
def certain[A](a: A) = new Random({seed: Long => (seed, a)})
Random[A]
是计算产生类型A的随机值 . 方法 flatMap
,_ _1181386_和函数 unit
用于组合简单计算以构建更复杂的计算 . 例如,我们将组成两个 Random[Double]
来构建 Random[(Double, Double)]
.
一元随机点
现在当我们有一个单子时,我们准备重新审视 randomPoint
和 randomDouble
. 现在我们将它们定义为不同的函数,产生 Random[Double]
和 Random[(Double, Double)]
def randomDouble(): Random[Double] = new Random({seed: Long => ... })
def randomPoint(): Random[(Double, Double)] =
randomDouble().flatMap(x => randomDouble().flatMap(y => certain(x, y))
此实现比前一个更好,因为它使用通用工具( flatMap
和 certain
)组成两个 Random[Double]
和构建 Random[(Double, Double)]
的调用 .
现在可以重复使用此工具来构建更多生成随机值的函数 .
Pi的Monte-Carlo计算
现在我们可以使用 map
来测试一个随机点是否在圆圈中:
def randomCircleTest(): Random[Boolean] =
randomPoint().map {case (x, y) => x * x + y * y <= 1}
我们还可以根据 Random[A]
定义蒙特卡罗模拟
def monteCarlo(test: Random[Boolean], trials: Int): Random[Double] = ...
最后是计算PI的函数
def pi(trials: Int): Random[Double] = ....
所有这些功能都是纯粹的 . 只有当我们最终应用 pi
函数来获取pi的值时才会出现副作用 .
1 回答
你的方法很好,虽然有点复杂 . 我还建议你看看Paul Chiusano和Runar Bjarnason的第12章Functional Programming in Scala . 本章称为Purely Functional State,它展示了如何创建纯函数随机生成器并在其上定义其数据类型代数以获得完整的函数组合支持 .