假设我想用蒙特卡罗模拟计算Pi作为练习。
我正在编写一个函数,它随机选取一个正方形(0, 1), (1, 0)
中的一个点并测试该点是否在圆内。
import scala.math._
import scala.util.Random
def circleTest() = {
val (x, y) = (Random.nextDouble, Random.nextDouble)
sqrt(x*x + y*y) <= 1
}
然后我正在编写一个函数,它将测试函数和试验次数作为参数,并返回测试结果为真的试验部分。
def monteCarlo(trials: Int, test: () => Boolean) =
(1 to trials).map(_ => if (test()) 1 else 0).sum * 1.0 / trials
......我可以计算Pi
monteCarlo(100000, circleTest) * 4
现在我想知道monteCarlo
功能是否可以改进。你如何写monteCarlo
有效且可读?
例如,由于试用次数很多,因此值得使用view
或iterator
代替Range(1, trials)
和reduce
代替map
sum
?
答案 0 :(得分:8)
值得注意的是Random.nextDouble
是副作用 - 当你调用它时它会改变随机数生成器的状态。这对你来说可能不是一个问题,但由于这里已有五个答案,我认为添加一个纯粹功能性的东西不会有任何损害。
首先,您需要一个随机数生成monad实现。幸运的是,NICTA提供了与Scalaz集成的a really nice one。您可以像这样使用它:
import com.nicta.rng._, scalaz._, Scalaz._
val pointInUnitSquare = Rng.choosedouble(0.0, 1.0) zip Rng.choosedouble(0.0, 1.0)
val insideCircle = pointInUnitSquare.map { case (x, y) => x * x + y * y <= 1 }
def mcPi(trials: Int): Rng[Double] =
EphemeralStream.range(0, trials).foldLeftM(0) {
case (acc, _) => insideCircle.map(_.fold(1, 0) + acc)
}.map(_ / trials.toDouble * 4)
然后:
scala> val choosePi = mcPi(10000000)
choosePi: com.nicta.rng.Rng[Double] = com.nicta.rng.Rng$$anon$3@16dd554f
还没有计算出来 - 我们刚刚建立了一个计算,它会在执行时随机生成我们的值。为方便起见,让我们在IO
monad中的现场执行:
scala> choosePi.run.unsafePerformIO
res0: Double = 3.1415628
这不是最高性能的解决方案,但它足够好,对许多应用程序来说可能不是问题,参考透明度可能是值得的。
答案 1 :(得分:3)
基于流的版本,另一种选择。我认为这很清楚。
def monteCarlo(trials: Int, test: () => Boolean) =
Stream
.continually(if (test()) 1.0 else 0.0)
.take(trials)
.sum / trials
(sum
并非专门针对流,但实施(在TraversableOnce中)只调用foldLeft
专用的&#34;允许GC沿途收集。&# 34;因此.sum不会强制对流进行评估,因此不会立即将所有试验保留在内存中)
答案 2 :(得分:2)
我发现以下递归版本没有问题:
def monteCarlo(trials: Int, test: () => Boolean) = {
def bool2double(b: Boolean) = if (b) 1.0d else 0.0d
@scala.annotation.tailrec
def recurse(n: Int, sum: Double): Double =
if (n <= 0) sum / trials
else recurse(n - 1, sum + bool2double(test()))
recurse(trials, 0.0d)
}
答案 3 :(得分:2)
还有一个foldLeft版本:
def monteCarloFold(trials: Int, test: () => Boolean) =
(1 to trials).foldLeft(0.0d)((s,i) => s + (if (test()) 1.0d else 0.0d)) / trials
这比问题中的map
版本更有效。
答案 4 :(得分:1)
使用尾递归可能是一个想法:
def recMonteCarlo(trials: Int, currentSum: Double, test:() => Boolean):Double = trials match {
case 0 => currentSum
case x =>
val nextSum = currentSum + (if (test()) 1.0 else 0.0)
recMonteCarlo(trials-1, nextSum, test)
def monteCarlo(trials: Int, test:() => Boolean) = {
val monteSum = recMonteCarlo(trials, 0, test)
monteSum / trials
}
答案 5 :(得分:1)
在并行集合上使用aggregate
,如下所示,
def monteCarlo(trials: Int, test: () => Boolean) = {
val pr = (1 to trials).par
val s = pr.aggregate(0)( (a,_) => a + (if (test()) 1 else 0), _ + _)
s * 4.0 / trials
}
其中部分结果与其他测试计算并行计算。