用Monadic方法估算Scala中的PI

drs*_*der 3 monads scala scala-cats

我正在尝试了解如何在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
Run Code Online (Sandbox Code Playgroud)

我怀疑我正在replicateMApplicative猫的单子中寻找某种东西,但是我不确定如何排列类型或如何以不会在内存中累积中间结果的方式来做。或者,是否有一种方法可以通过for理解来迭代建立Points?

Mat*_*zok 5

您是否想以堆栈安全的方式使用monad进行迭代,然后tailRecMMonad类型类中实现了一个方法:

// 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)
Run Code Online (Sandbox Code Playgroud)

它使用了基于名称的参数,因为您可以尝试向那里传递类似的东西Future(即使那Future是不合法的),这很急切,因此您将不得不一次又一次地评估同一件事。至少使用名字参数,您有机会传递一个副作用随机的配方。当然,如果我们使用OptionList作为持有我们的“随机”数字的单子,我们也应该期待有趣的结果。

正确的解决方案是使用可以确保对此F[A]进行延迟评估的方法,并且每次您需要内部提供一个值时,都可以评估内部的任何副作用。为此,您基本上必须使用一些Effect类型类,例如SyncCats 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
Run Code Online (Sandbox Code Playgroud)

另外,如果您不太关心纯度,则可以传递副作用函数或对象,而不是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
}
Run Code Online (Sandbox Code Playgroud)