以函数式方式在 Scala 中重写集合填充模拟

Re-writing collection-populating simulation in Scala in a functional way

进行一个非常简单的模拟,我们对一个人在一年内的存活或死亡进行建模 - 根据生成的随机数,我们决定该人是否存活一年:

case class PersonEntry(pid: Long, year: Short, age: Short, status: Byte) {

  // return an entry where person survives and gets 1 year older
  def Mature(): PersonEntry = {
    PersonEntry(pid, (year + 1).toShort, (age + 1).toShort, status)
  }

  // return an entry where person dies (status is 0)
  def Kill(): PersonEntry = {
    PersonEntry(pid, (year + 1).toShort, (age + 1).toShort, 0)
  }

  // based on a random number decide if return Matured or Killed
  def Simulate(p: Double = 0.5): PersonEntry = {
    val rnd = scala.util.Random
    if (rnd.nextDouble < p) this.Mature() else this.Kill() 
  }

}

据此我们可以得到一个模拟1年生存概率为95%的同一个人的新条目:

val per1 = PersonEntry(1, 2018, 20, 1)
val per2 = per1.Simulate(0.95)

我们接下来要做的是创建一个任意年限的模拟。到目前为止,我为此创建的是一种非常直接的方法:

import scala.collection.mutable.ListBuffer

case class PersonSimulation(entries: ListBuffer[PersonEntry]) {
 def Simulate(n: Int = 1, p: Double = 0.5): Unit = {
   for (i <- List.range(1, n)) {
     this.entries += this.entries.last.Simulate(p)
   }
 }
}

现在我们可以做:

val per = PersonEntry(1, 2018, 20, 1)
val sim = PersonSimulation(ListBuffer(per))
sim.Simulate(100, 0.95)

// look at the result
println(sim)

PersonSimulation.Simulate 方法可以完成这项工作,但根本不起作用,因为它将新元素添加到 entries ListBuffer.

我们如何才能有效地以函数式方式重写 PersonSimulation

它与 Simulate 的签名并不完全相同,但您可以使用 iterate 方法相当优雅地完成此类操作,您可以在 List、[=17] 上找到=],等等:

val entry = PersonEntry(1, 2018, 20, 1)
val entries = List.iterate(entry, 100)(_.Simulate(0.95))

这表示 "start with entry, call .Simulate(0.95) on it, then call .Simulate(0.95) on the result you get, and then on that result, for 100 times in a row, collecting the results in a list",它看起来像这样的例子:

scala> entries.foreach(println)
PersonEntry(1,2018,20,1)
PersonEntry(1,2019,21,1)
PersonEntry(1,2020,22,1)
PersonEntry(1,2021,23,1)
PersonEntry(1,2022,24,1)
PersonEntry(1,2023,25,1)
PersonEntry(1,2024,26,1)
PersonEntry(1,2025,27,1)
PersonEntry(1,2026,28,1)
PersonEntry(1,2027,29,1)
PersonEntry(1,2028,30,1)
PersonEntry(1,2029,31,1)
PersonEntry(1,2030,32,1)
PersonEntry(1,2031,33,0)
PersonEntry(1,2032,34,0)
...

有了Stream你甚至不用提前设置迭代次数:

val entries = Stream.iterate(entry)(_.Simulate(0.95))

现在你的模拟中有无穷无尽的岁月流,你可以拿出一大块来看看:

scala> entries.take(10).foreach(println)
PersonEntry(1,2018,20,1)
PersonEntry(1,2019,21,1)
PersonEntry(1,2020,22,1)
PersonEntry(1,2021,23,1)
PersonEntry(1,2022,24,1)
PersonEntry(1,2023,25,1)
PersonEntry(1,2024,26,0)
PersonEntry(1,2025,27,0)
PersonEntry(1,2026,28,0)
PersonEntry(1,2027,29,0)

请注意,这些解决方案都不是纯粹的功能,因为您依赖于随机数生成器,每次您都会得到不同的结果运行 程序,但它的功能在于它避免使用可变集合来收集结果。

如果你想使用函数式方式——使用不可变数据结构。 对案例 classes 使用方法复制。 您创建了另一个案例 class 但您可以创建隐式方法。

我们在这里:

import scala.util.Random

case class PersonEntry(pid: Long, year: Short, age: Short, status: Byte) {

  // return an entry where person survives and gets 1 year older
  def Mature: PersonEntry = {
    copy(year = (year + 1).toShort, age = (age + 1).toShort)
  }

  // return an entry where person dies (status is 0)
  def Kill: PersonEntry = {
    copy(year = (year + 1).toShort, age = (age + 1).toShort, status = 0)
  }

  // based on a random number decide if return Matured or Killed
  def Simulate(p: Double = 0.5): PersonEntry = {
    val rnd = Random.nextDouble
    if (rnd < p) Mature else Kill
  }

}

implicit class PersonListExt(l: List[PersonEntry]) {
  def simulate(n: Int = 1, p: Double = 0.5): Map[Int, List[PersonEntry]] = {
    (1 to n).map(_ -> l.map(_.Simulate(p))).toMap
  }
}

val simulateMap: Map[Int, List[PersonEntry]]  = (1 to 10).map(i =>
  PersonEntry(i, 2018.toShort, Random.nextInt(50).toShort, 1)
).toList.simulate()