Как реализовать оценку стоимости отложенных вычислений с помощью flatMap? - PullRequest
0 голосов
/ 19 февраля 2019

Я реализовал класс Calculation, который принимает 2 параметра: вход для расчета, который является параметром вызова по имени, а также стоимость.Когда я пытаюсь 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 голосов
/ 19 февраля 2019

Вам просто нужно немного больше лени, чтобы стоимость не жадно оценивалась в 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 дважды подряд приведет к тому, что F1 и F2 будут напечатаны в первый раз, и только F2 во второй раз), но при этом не будет возникать побочный эффект при определении f2.

0 голосов
/ 19 февраля 2019

Если я понимаю ваше требование

отложить все в flatMap и по-прежнему правильно указать общую стоимость

, тогда вы хотите вычислить оценку для общих затрат до выполнения каких-либо вычислений.Я не понимаю, как это должно работать с подписью flatMap[A](f: R => Calculation[A]): Calculation[A] - ваш cost привязан к Calculation[A], а ваш Calculation[A] зависит от конкретного экземпляра 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 единиц без каких-либо вычислений.
  • Только когда выinvoke .result вычисляет ли он фактический результат 10.0

По общему признанию, он не очень полезен ни для чего, кроме очень грубых оценок порядка величин.Это настолько грубо, что даже использование Int s едва ли имеет какой-либо смысл.


Непостоянные затраты за шаг

Вы можете сделать свои оценки затрат более точными, отслеживая размероцените следующим образом:

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)

Вывод выглядит обнадеживающим:

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)

Обратите внимание, что подход не ограничивается списками и целыми числами: оценки размера могут быть произвольно сложными.Например, они могут быть размерами матриц или тензоров.На самом деле, они вовсе не должны быть размеров .Эти оценки также могут содержать любые другие виды «статических консервативных оценок», такие как типы или логические предикаты.


Непостоянные затраты с использованием Writer

Использование Writer монаду от Cats, мы можем выразить ту же идею более кратко, заменив два метода outputSizeEstimate и costs на Step одним методом, который принимает Int и возвращает Writer[Int, Int]:

  • Writer с .value соответствует оценке размера для вывода
  • Writer с .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)
}

Вывод остается точно таким же, как в предыдущем разделе.


PS: Я думаю, что придумалболее краткий способ суммировать весь этот ответ:

Используйте категорию продукта обычной внешней среды Scala (типы и функции) с моноидом эндоморфизмов на объекте Int в категории Клейсли Writer[Int, ?].

В какой-то гипотезеНеверный язык, ответ мог бы быть:

Использовать Sc * End{Kl(Writer[Int, ?])}[Int].

0 голосов
/ 19 февраля 2019

Прежде всего, нет причин заново изобретать свои собственные Functor и FlatMap, и я настоятельно рекомендую вам использовать существующую реализацию.

Если вам нужны отложенные вычисления, чем cats.Writer[Int, ?]ваш друг.

С его поддержкой вы можете записать свои расходы, а также получить экземпляры функторов и монад.

Позвольте мне привести вам пример.Начнем с некоторой начальной стоимости

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