[英]Monadic approach to estimating PI in scala
I'm trying to understand how to leverage monads in scala to solve simple problems as way of building up my familiarity. 我试图了解如何在scala中利用monad来解决一些简单的问题,以此来增强我的熟悉度。 One simple problem is estimating PI using a functional random number generator.
一个简单的问题是使用函数随机数生成器估算PI。 I'm including the code below for a simple stream based approach.
我将在下面的代码中包含一个基于流的简单方法。
I'm looking for help in translating this to a monadic approach. 我正在寻求帮助,以将其转换为单子方法。 For example, is there an idiomatic way convert this code to using the state (and other monads) in a stack safe way?
例如,是否有惯用的方法将此代码转换为以堆栈安全的方式使用状态(和其他monad)?
trait RNG {
def nextInt: (Int, RNG)
def nextDouble: (Double, RNG)
}
case class Point(x: Double, y: Double) {
val isInCircle = (x * x + y * y) < 1.0
}
object RNG {
def nonNegativeInt(rng: RNG): (Int, RNG) = {
val (ni, rng2) = rng.nextInt
if (ni > 0) (ni, rng2)
else if (ni == Int.MinValue) (0, rng2)
else (ni + Int.MaxValue, rng2)
}
def double(rng: RNG): (Double, RNG) = {
val (ni, rng2) = nonNegativeInt(rng)
(ni.toDouble / Int.MaxValue, rng2)
}
case class Simple(seed: Long) extends RNG {
def nextInt: (Int, RNG) = {
val newSeed = (seed * 0x5DEECE66DL + 0xBL) & 0xFFFFFFFFFFFFL
val nextRNG = Simple(newSeed)
val n = (newSeed >>> 16).toInt
(n, nextRNG)
}
def nextDouble: (Double, RNG) = {
val (n, nextRNG) = nextInt
double(nextRNG)
}
}
}
object PI {
import RNG._
def doubleStream(rng: Simple):Stream[Double] = rng.nextDouble match {
case (d:Double, next:Simple) => d #:: doubleStream(next)
}
def estimate(rng: Simple, iter: Int): Double = {
val doubles = doubleStream(rng).take(iter)
val inside = (doubles zip doubles.drop(3))
.map { case (a, b) => Point(a, b) }
.filter(p => p.isInCircle)
.size * 1.0
(inside / iter) * 4.0
}
}
// > PI.estimate(RNG.Simple(10), 100000)
// res1: Double = 3.14944
I suspect I'm looking for something like replicateM
from the Applicative
monad in cats but I'm not sure how to line up the types or how to do it in a way that doesn't accumulate intermediate results in memory. 我怀疑我在寻找类似
replicateM
从Applicative
猫的单子,但我不知道怎么排队的类型或如何做到这一点在不积聚在内存中间结果的方式。 Or, is there a way to do it with a for
comprehension that can iteratively build up Point
s? 或者,是否有一种方法可以借助
for
理解来迭代构建Point
?
Id you want to iterate using monad in a stack safe way, then there is a tailRecM
method implemented in Monad
type class: 如果您想以安全的方式迭代使用monad,那么
Monad
类型类中实现了tailRecM
方法:
// assuming random generated [-1.0,1.0]
def calculatePi[F[_]](iterations: Int)
(random: => F[Double])
(implicit F: Monad[F]): F[Double] = {
case class Iterations(total: Int, inCircle: Int)
def step(data: Iterations): F[Either[Iterations, Double]] = for {
x <- random
y <- random
isInCircle = (x * x + y * y) < 1.0
newTotal = data.total + 1
newInCircle = data.inCircle + (if (isInCircle) 1 else 0)
} yield {
if (newTotal >= iterations) Right(newInCircle.toDouble / newTotal.toDouble * 4.0)
else Left(Iterations(newTotal, newInCircle))
}
// iterates until Right value is returned
F.tailRecM(Iterations(0, 0))(step)
}
calculatePi(10000)(Future { Random.nextDouble }).onComplete(println)
It uses by-name param because you could try to pass there something like Future
(even though the Future
is not lawful), which are eager, so you would end up with evaluating the same thing time and time again. 它使用了别名参数,因为您可以尝试将诸如
Future
东西(即使Future
不合法)传递给那里,因为它们非常渴望,因此最终您会一次又一次地评估同一件事。 With by name param at least you have the chance of passing there a recipe for side-effecting random. 至少使用名字参数,您有机会传递一个副作用随机的配方。 Of course, if we use
Option
, List
as a monad holding our "random" number, we should also expect funny results. 当然,如果我们使用
Option
, List
作为持有我们的“随机”数字的单子,我们也应该期待有趣的结果。
The correct solution would be using something that ensures that this F[A]
is lazily evaluated, and any side effect inside is evaluated each time you need a value from inside. 正确的解决方案是使用某些方法来确保对
F[A]
的求值是延迟的,并且每次您需要内部的值时,都会评估内部的任何副作用。 For that you basically have to use some of Effects type classes, like eg Sync
from Cats Effects. 为此,您基本上必须使用一些Effect类型类,例如Cats Effects中的
Sync
。
def calculatePi[F[_]](iterations: Int)
(random: F[Double])
(implicit F: Sync[F]): F[Double] = {
...
}
calculatePi(10000)(Coeval( Random.nextDouble )).value
calculatePi(10000)(Task( Random.nextDouble )).runAsync
Alternatively, if you don't care about purity that much, you could pass side effecting function or object instead of F[Int]
for generating random numbers. 另外,如果您不太关心纯度,则可以传递副作用函数或对象而不是
F[Int]
来生成随机数。
// simplified, hardcoded F=Coeval
def calculatePi(iterations: Int)
(random: () => Double): Double = {
case class Iterations(total: Int, inCircle: Int)
def step(data: Iterations) = Coeval {
val x = random()
val y = random()
val isInCircle = (x * x + y * y) < 1.0
val newTotal = data.total + 1
val newInCircle = data.inCircle + (if (isInCircle) 1 else 0)
if (newTotal >= iterations) Right(newInCircle.toDouble / newTotal.toDouble * 4.0)
else Left(Iterations(newTotal, newInCircle))
}
Monad[Coeval].tailRecM(Iterations(0, 0))(step).value
}
Here is another approach that my friend Charles Miller came up with. 这是我的朋友查尔斯·米勒 ( Charles Miller)提出的另一种方法。 It's a bit more direct since it uses
RNG
directly but it follows the same approach provided by @Mateusz Kubuszok above that leverages Monad
. 因为它直接使用
RNG
所以它更直接一些,但它遵循@Mateusz Kubuszok提供的与利用Monad
相同的方法。
The key difference is that it leverages the State
monad so we can thread the RNG
state through the computation and generate the random numbers using the "pure" random number generator. 关键区别在于它利用
State
monad,因此我们可以通过计算对RNG
状态进行处理,并使用“纯”随机数生成器生成随机数。
import cats._
import cats.data._
import cats.implicits._
object PICharles {
type RNG[A] = State[Long, A]
object RNG {
def nextLong: RNG[Long] =
State.modify[Long](
seed ⇒ (seed * 0x5DEECE66DL + 0xBL) & 0xFFFFFFFFFFFFL
) >> State.get
def nextInt: RNG[Int] = nextLong.map(l ⇒ (l >>> 16).toInt)
def nextNatural: RNG[Int] = nextInt.map { i ⇒
if (i > 0) i
else if (i == Int.MinValue) 0
else i + Int.MaxValue
}
def nextDouble: RNG[Double] = nextNatural.map(_.toDouble / Int.MaxValue)
def runRng[A](seed: Long)(rng: RNG[A]): A = rng.runA(seed).value
def unsafeRunRng[A]: RNG[A] ⇒ A = runRng(System.currentTimeMillis)
}
object PI {
case class Step(count: Int, inCircle: Int)
def calculatePi(iterations: Int): RNG[Double] = {
def step(s: Step): RNG[Either[Step, Double]] =
for {
x ← RNG.nextDouble
y ← RNG.nextDouble
isInCircle = (x * x + y * y) < 1.0
newInCircle = s.inCircle + (if (isInCircle) 1 else 0)
} yield {
if (s.count >= iterations)
Right(s.inCircle.toDouble / s.count.toDouble * 4.0)
else
Left(Step(s.count + 1, newInCircle))
}
Monad[RNG].tailRecM(Step(0, 0))(step(_))
}
def unsafeCalculatePi(iterations: Int) =
RNG.unsafeRunRng(calculatePi(iterations))
}
}
Thanks Charles & Mateusz for your help! 感谢Charles&Mateusz的帮助!
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.