简体   繁体   English

用Monadic方法估算Scala中的PI

[英]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. 我怀疑我在寻找类似replicateMApplicative猫的单子,但我不知道怎么排队的类型或如何做到这一点在不积聚在内存中间结果的方式。 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. 当然,如果我们使用OptionList作为持有我们的“随机”数字的单子,我们也应该期待有趣的结果。

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM