如何使用“ flatMap”为延迟的计算实现成本估算?

时间:2019-02-18 21:27:37

标签: scala monads

我实现了一个带有两个参数的Calculation类:一个计算输入,它是一个按名称调用的参数,也是一个成本。当我尝试进行flatMap计算时,将执行第一部分。是否可以推迟flatMap中的所有内容并仍然提供总费用?

class Calculation[+R](input: => R, val cost: Int = 0) {
    def value: R = input

    def map[A](f: R => A): Calculation[A] =
        new Calculation(f(input), cost)

    def flatMap[A](f: R => Calculation[A]): Calculation[A] = {
        val step = f(input)
        new Calculation(step.value, cost + step.cost)
    }
}

object Rextester extends App {
    val f1 = new Calculation({
        println("F1")
        "F1"
    })

    val f2 = f1.flatMap(s => new Calculation({
        println("F2")
        s + " => F2"
    }))

    println(f2.cost)
}

一旦声明了f2(被调用flatMap),我们可以看到将打印“ F1”。印刷成本是"15",这是正确的,但我希望完全推迟实际计算,这意味着在计算成本时我不应该看到f1被执行。

3 个答案:

答案 0 :(得分:3)

您只需要多一点懒惰,这样flatMap中就不会急切评估成本:

class Calculation[+R](input: => R, c: => Int = 0) {
  def value: R = input
  lazy val cost: Int = c

  def map[A](f: R => A): Calculation[A] =
    new Calculation(f(input), cost)

  def flatMap[A](f: R => Calculation[A]): Calculation[A] = {
    lazy val step = f(value)
    new Calculation(step.value, cost + step.cost)
  }
}

请注意,这仍然可能不完全具有您想要的语义(例如,连续两次调用f2.value将导致F1F2都被第一次打印,并且仅F2),但是在定义f2时确实不会产生副作用。

答案 1 :(得分:2)

如果我了解您的要求

  

将flatMap中的所有内容延期并仍然提供总费用

正确地,那么您要在进行任何计算之前 计算总成本的估算值。我看不到签名flatMap[A](f: R => Calculation[A]): Calculation[A]应该如何工作-您的cost已附加到Calculation[A],而您的Calculation[A]取决于{{ 1}},因此您无法在计算R之前计算成本。


计算步骤的不变成本

这是一个完全不同的建议:

R

特征sealed trait Step[-A, +B] extends (A => B) { outer => def estimatedCosts: Int def andThen[U >: B, C](next: Step[U, C]): Step[A, C] = new Step[A, C] { def apply(a: A): C = next(outer(a)) def estimatedCosts = outer.estimatedCosts + next.estimatedCosts } def result(implicit u_is_a: Unit <:< A): B = this(u_is_a(())) } type Computation[+R] = Step[Unit, R] 表示一个计算步骤,其成本不取决于输入。它实际上只是一个附加了整数值的Step。这样,您的Function[A, B]就变成了一种特殊情况,即Computation[R]

这里是使用方式:

Step[Unit, R]

如果运行它,您将获得:

val x = new Step[Unit, Int] {
  def apply(_u: Unit) = 42
  def estimatedCosts = 0
}

val mul = new Step[Int, Int] {
  def apply(i: Int) = {
    println("<computing> adding is easy")
    i + 58
  }
  def estimatedCosts = 10
}

val sqrt = new Step[Int, Double] {
  def apply(i: Int) = {
    println("<computing> finding square roots is difficult")
    math.sqrt(i)
  }
  def estimatedCosts = 50
}

val c: Computation[Double] = x andThen mul andThen sqrt

println("Estimated costs: " + c.estimatedCosts)
println("(nothing computed so far)")
println(c.result)

它的作用如下:

  • 它从值Estimated costs: 60 (nothing computed so far) <computing> adding is easy <computing> finding square roots is difficult 10.0 开始,向其加42,然后计算和的平方根
  • 加法设置为成本58单位,平方根成本为10
  • 它为您提供50个单位的成本估算,而无需执行任何计算。
  • 仅当您调用60时,它才会计算实际结果.result

诚然,除了非常粗略的数量级估计之外,它对其他任何事情都不是很有用。它是如此的粗糙以至于即使使用10.0也几乎没有任何意义。


每步的非恒定成本

通过跟踪大小估算值,可以使成本估算更加准确:

Int

输出看起来令人鼓舞:

trait Step[-A, +B] extends (A => B) {
  def outputSizeEstimate(inputSizeEstimate: Int): Int
  def costs(inputSizeEstimate: Int): Int
}


trait Computation[+R] { outer =>
  def result: R
  def resultSize: Int
  def estimatedCosts: Int

  def map[S](step: Step[R, S]): Computation[S] = new Computation[S] {
    def result: S = step(outer.result)
    def estimatedCosts: Int = outer.estimatedCosts + step.costs(outer.resultSize)
    def resultSize: Int = step.outputSizeEstimate(outer.resultSize)
  }
}

val x = new Computation[List[Int]] {
  def result = (0 to 10).toList
  def resultSize = 10
  def estimatedCosts = 10
}

val incrementEach = new Step[List[Int], List[Int]] {
  def outputSizeEstimate(inputSize: Int) = inputSize
  def apply(xs: List[Int]) = {
    println("incrementing...")
    xs.map(1.+)
  }
  def costs(inputSize: Int) = 3 * inputSize
}

val timesSelf = new Step[List[Int], List[(Int, Int)]] {
  def outputSizeEstimate(n: Int) = n * n
  def apply(xs: List[Int]) = {
    println("^2...")
    for (x <- xs; y <- xs) yield (x, y)
  }
  def costs(n: Int) = 5 * n * n
}

val addPairs = new Step[List[(Int, Int)], List[Int]] {
  def outputSizeEstimate(n: Int) = n
  def apply(xs: List[(Int, Int)]) = {
    println("adding...")
    xs.map{ case (a, b) => a + b }
  }
  def costs(n: Int) = 7 * n
}

val y = x map incrementEach map timesSelf map addPairs

println("Estimated costs (manually):      " + (10 + 30 + 500 + 700))
println("Estimated costs (automatically): " + y.estimatedCosts)
println("(nothing computed so far)")
println(y.result)

请注意,该方法不限于列表和整数:大小估计值可以任意复杂。例如,它们可以是矩阵或张量的尺寸。实际上,它们根本不必是 size 。这些估计也可以包含任何其他类型的“静态保守估计”,例如类型或逻辑谓词。


使用Estimated costs (manually): 1240 Estimated costs (automatically): 1240 (nothing computed so far) incrementing... ^2... adding... List(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ...[omitted]..., 20, 21, 22) 的非恒定成本

使用Cats的Writer单子,我们可以通过用一种方法替换Writer上的outputSizeEstimatecosts这两种方法来更简洁地表达相同的想法接受一个Step并返回一个Int

  • Writer[Int, Int]的{​​{1}}对应于输出的大小估计
  • Writer的{​​{1}}对应于该步骤的费用(可能取决于输入的大小)

完整代码:

.value

输出与上一节完全相同。


PS:我想我想出了一种更简洁的方法来总结整个答案:

  

将普通环境Scala类别(类型和函数)的产品类别与Writer的Kleisli类别中对象.written上的同构同形体一起使用。

用某种假设的语言,答案可能是:

  

使用import cats.data.Writer import cats.syntax.writer._ import cats.instances.int._ object EstimatingCosts extends App { type Costs = Int type Size = Int trait Step[-A, +B] extends (A => B) { def sizeWithCosts(inputSizeEstimate: Size): Writer[Costs, Size] } object Step { def apply[A, B] (sizeCosts: Size => (Size, Costs)) (mapResult: A => B) : Step[A, B] = new Step[A, B] { def apply(a: A) = mapResult(a) def sizeWithCosts(s: Size) = { val (s2, c) = sizeCosts(s); Writer(c, s2) } } } trait Computation[+R] { outer => def result: R def sizeWithCosts: Writer[Costs, Size] def size: Size = sizeWithCosts.value def costs: Costs = sizeWithCosts.written def map[S](step: Step[R, S]): Computation[S] = new Computation[S] { lazy val result: S = step(outer.result) lazy val sizeWithCosts = outer.sizeWithCosts.flatMap(step.sizeWithCosts) } } object Computation { def apply[A](initialSize: Size, initialCosts: Costs)(a: => A) = { new Computation[A] { lazy val result = a lazy val sizeWithCosts = Writer(initialCosts, initialSize) } } } val x = Computation(10, 10){ (0 to 10).toList } val incrementEach = Step(n => (n, 3 * n)){ (xs: List[Int]) => println("incrementing...") xs.map(1.+) } val timesSelf = Step(n => (n * n, 5 * n * n)) { (xs: List[Int]) => println("^2...") for (x <- xs; y <- xs) yield (x, y) } val addPairs = Step(n => (n, 7 * n)) { (xs: List[(Int, Int)]) => println("adding...") xs.map{ case (a, b) => a + b } } val y = x map incrementEach map timesSelf map addPairs println("Estimated costs (manually): " + (10 + 30 + 500 + 700)) println("Estimated costs (automatically): " + y.costs) println("(nothing computed so far)") println(y.result) }

答案 2 :(得分:-1)

首先,没有理由重新发明自己的FunctorFlatMap,我强烈建议您使用现有的实现。

如果您需要延迟计算,而不是cats.Writer[Int, ?]是您的朋友。

有了它的支持,您可以编写成本以及获得函子和monad实例。

让我给你一个例子。我们从一些初始费用开始

val w = Writer.put("F1")(0)
w.flatMap(v => Writer.value(v + "F2"))
相关问题