【问题标题】:Monadic approach to estimating PI in scala在 scala 中估计 PI 的一元方法
【发布时间】:2019-09-06 16:38:37
【问题描述】:

我试图了解如何利用 scala 中的 monad 来解决简单的问题,以此来增加我的熟悉度。一个简单的问题是使用功能随机数生成器估计 PI。我将下面的代码包含在一个简单的基于流的方法中。

我正在寻求帮助,将其转化为单子方法。例如,是否有一种惯用的方式将此代码转换为以堆栈安全的方式使用状态(和其他 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

我怀疑我正在从 cat 中的 Applicative monad 中寻找类似 @​​987654322@ 的东西,但我不确定如何排列类型或如何以不会累积中间结果的方式进行排列在记忆中。或者,有没有办法通过 for 理解来迭代地构建 Points?

【问题讨论】:

    标签: scala monads scala-cats


    【解决方案1】:

    如果您想以堆栈安全的方式使用 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)
    

    它使用别名参数,因为您可以尝试将 Future 之类的东西传递给那里(即使 Future 是不合法的),这是急切的,因此您最终会一次又一次地评估相同的事情再次。通过名称参数,您至少有机会在那里传递一个副作用随机的配方。当然,如果我们使用Option、List 作为包含“随机”数的 monad,我们也应该期待有趣的结果。

    正确的解决方案是使用一些东西来确保这个F[A] 被懒惰地评估,并且每次你需要一个来自内部的值时评估内部的任何副作用。为此,您基本上必须使用一些效果类型类,例如Sync 来自 Cats Effects。

    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
    

    或者,如果您不太关心纯度,您可以传递副作用函数或对象而不是 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
    }
    

    【讨论】:

    • 谢谢!您是否看到了一种从上面注入功能性RNG.Simple 并让它通过计算而不是使用有状态的Random.nextDouble 携带状态的方法?
    • 在功能方面,您可以使用一些 IO monad 来封装副​​作用(Cats' Effects IO、Monix's Task、Scalaz ZIO)。然后您将收到IO[Result] 的结果。并在main 中运行该 IO。 State monad 只是函数S =&gt; (S,A) 的组合,您仍然需要在某处执行一些副作用。如果你只是生成了一次随机数据序列,然后纯粹将它变成一个结果,那么它只是一个foldLeft,你不需要一个monad。
    • 我尝试了您在上面提供的第一段代码,但无法编译。错误是value flatMap is not a member of type parameter F[Double] x &lt;- random(y 相同)。我错过了什么吗?
    • 如果您正在使用类型类并且想要使用语法。您还必须导入此语法。例如。 cats.Monad + cats.syntax.monad._ 或 scalaz.Monad + scalaz.syntax.monad._。 (或cats._ + cats.implicits._ 或scalaz._ + Scalaz._)。
    【解决方案2】:

    这是我的朋友Charles Miller 提出的另一种方法。它更直接一些,因为它直接使用RNG,但它遵循上面@Mateusz Kubuszok 提供的利用Monad 的相同方法。

    关键区别在于它利用了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))
      }
    }
    

    感谢 Charles 和 Mateusz 的帮助!

    【讨论】:

      猜你喜欢
      • 2023-04-03
      • 2023-02-08
      • 1970-01-01
      • 1970-01-01
      • 2018-09-12
      • 1970-01-01
      • 2012-02-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多