【问题标题】:Satisfying Abstract Trait Requirements with Futures用期货满足抽象特征要求
【发布时间】:2013-07-15 21:34:04
【问题描述】:

我有一个抽象的特征,对计算有一些要求,然后是对这些计算结果的一些函数。我想让这个特性保持简单,以便于理解和测试。

trait Calculator {
  def hardToCalculate1: Int
  def hardToCalculate2: Int
  def hardToCalculate3: Int

  def result1 = hardToCalculate1 + hardToCalculate2
  def result2 = hardToCalculate2 + hardToCalculate3
  def result3 = hardToCalculate1 + hardToCalculate3
}

当我实例化一个Calculator 时,我将使用Futures 来计算那些hardToCalculate 值。假设它们看起来像这样:

def f1 = future {
    println("calculating 1")
    1
}
def f2 = future {
    println("calculating 2")
    2
}
def f3 = future {
    println("calculating 3")
    3
}

所以,我可以像这样构造一个Future[Calculator]

val myCalc = for {
  m1 <- f1
  m2 <- f2
  m3 <- f3
} yield new Calculator {
  lazy val hardToCalculate1 = m1
  lazy val hardToCalculate2 = m2
  lazy val hardToCalculate3 = m3
}

那么,我可能会像这样使用myCalc

myCalc onComplete {
  case Success(calc) => println("Result: " + calc.result1)
}

但是当我这样做时,我得到了这个:

calculating 1
calculating 2
calculating 3
Result: 3

我只想在我正在进行的计算确实需要这些期货时才执行它们。尽管我用lazy val 声明了hardToCalculates,但所有三个都是在执行Future[Calculator].onComplete 时计算的。

一种方法是这样的:

val calculator = new Calculator {
    lazy val hardToCalculate1 = Await.result(f1, 10 seconds)
    lazy val hardToCalculate2 = Await.result(f2, 10 seconds)
    lazy val hardToCalculate3 = Await.result(f3, 10 seconds)
}
println("result: " + calculator.result1)

这会产生我想要的:

calculating 1
calculating 2
result: 3

但现在我有所有Await 阻塞。我真正想要的是一个Future[Calculator],它将以一种懒惰的方式执行期货。如果不将 Futures 引入我的 Calculator 特征中,这可能吗?关于如何在这里获得我想要的东西还有其他建议吗?

(A gist with all of the above code is here.)

【问题讨论】:

    标签: scala akka future


    【解决方案1】:

    如果您创建一个Future(使用scala.concurrent.future),无论您做什么,它都会被计算出来。所以你需要一个完全不同的策略。

    此外,您的界面甚至不允许远程确定您将实际使用哪些数据。 myCalc的计算应该如何知道onComplete中你只会使用result1

    你可以:

    • 只使用惰性值:

      val calculator = new Calculator {
        lazy val hardToCalculate1 = {
          println("calculating 1")
          1
        }
        // ...
      }
      

      专业版:简单
      缺点:不是异步的

    • 封装Future 以允许请求计算:

      class ReqFuture[T](body: () => T) {
        lazy val fut = future { body() }
      }
      

      但是现在你仍然有myCalc 会请求并等待所有这些的问题。所以你必须在Calculator中介绍ReqFutures

      trait Calculator {
          def hardToCalculate1: ReqFuture[Int]
          // ...
          def result1 = for {
            h1 <- hardToCalculate1.fut
            h2 <- hardToCalculate2.fut
          } yield h1 + h2
      }
      

      专业人士:当您调用 result1 时,只会计算您需要的内容(但仍然只计算一次)。
      骗子:result1 现在是Future[Int]。所以Futures已经完全渗透了你的Calculator

    如果您无法影响Calculator(我怀疑)并且无法更改result1,2,3 的代码,那么不幸的是,据我所知,您无法使执行变得懒惰和异步。

    【讨论】:

    • 感谢您的周到回答。我可以影响 Calculator,但它会包含复杂的业务逻辑,并且我想将它(以及将编写/测试它的人)从 Futures 中屏蔽掉,以便理解语法等。但是,我想将它与异步代码一起使用。同意需要新策略。
    • @brandon 我在上班时想到的一些事情:您可以编写一个包装器,使用宏和Dynamic,看起来是同步的,但实际上是 flatMaps the Futures。 (你也可以不用魔法来做到这一点,但这很烦人,因为你必须为你的类编写一个完整但微不足道的适配器)。
    • 这正是我正在考虑的那种事情......我正在研究宏,但我从未研究过Dynamic。也许这就是拼图的最后一块。
    • @brandon 看看这里:stackoverflow.com/questions/17580781/… Travis 尝试使用动态来创建自定义类型。他遇到了必须多次读取结构存储的问题。在你的情况下,这本身就是一个 Scala 类型,所以你应该没问题。
    • @brandon 在这里查看一个非常简单的示例:github.com/gzm0/monwrap 我将尝试将其扩展到可用的代码片段(允许组合 MWs,将其转换为 flatMaps ) 因为我前段时间在一个项目中遇到过类似的问题。
    【解决方案2】:

    我想尝试新的Async API,这是一个很好的测试。事实上,在 Async Github 主页上有一个与您的用例非常接近的示例。

    Async 是一个SIP,并且可能在某些时候成为标准 Scala 的一部分。

    除了使用await,这里的想法是你有一个抽象的add() 方法,它在后台使用异步逻辑。这样它就对Calculator 开发人员隐藏了。

    我个人也会添加一个异步版本的 API,以便 Futures 可以从 Calculator 中传递出来,与其他 Futures 组合。

    trait Calculator {
      def hardToCalculate1: Int
      def hardToCalculate2: Int
      def hardToCalculate3: Int
    
      def add(a: => Int, b: => Int): Int
    
      def result1 = add(hardToCalculate1, hardToCalculate2)
      def result2 = add(hardToCalculate2, hardToCalculate3)
      def result3 = add(hardToCalculate1, hardToCalculate3)
    }
    
    object So17677728 extends App with Calculator {
    
      override def add(a: => Int, b: => Int): Int = {
        val start = System.currentTimeMillis
    
        val res = Await.result(asyncAdd(a, b), 2000 millis)
    
        val end = System.currentTimeMillis
    
        println(s"Total time: ${end - start} ms")
    
        res
      }
    
      def asyncAdd(a: => Int, b: => Int): Future[Int] = {
        async {
          val fa = Future(a)
          val fb = Future(b)
          await(fa) + await(fb)
        }
      }
    
      val random = Random
      val maxSleep = 1000
    
      def hardToCalculate1: Int = htc(1)
      def hardToCalculate2: Int = htc(2)
      def hardToCalculate3: Int = htc(3)
    
      def htc(n: Int) = {
        val sleepMs = random.nextInt(1000)
        println(s"$n sleeping for $sleepMs ms")
        Thread.sleep(sleepMs)
        println(s"$n done sleeping")
        n
      }
    
      println(s"result1: $result1\n")
      println(s"result2: $result2\n")
      println(s"result3: $result3\n")
    }
    

    输出

    1 sleeping for 438 ms
    2 sleeping for 244 ms
    2 done sleeping
    1 done sleeping
    Total time: 497 ms
    result1: 3
    
    3 sleeping for 793 ms
    2 sleeping for 842 ms
    3 done sleeping
    2 done sleeping
    Total time: 844 ms
    result2: 5
    
    3 sleeping for 895 ms
    1 sleeping for 212 ms
    1 done sleeping
    3 done sleeping
    Total time: 896 ms
    result3: 4
    

    或者在add 中,您可以使用Futures 和一个for comprehension,而不是async/await

    【讨论】:

    • 谢谢。仍然在这个问题上打我的头。似乎我应该能够创建某种包装类,允许我在 Calculator 中使用简单、直接的函数,但异步提供硬值(并检索结果)。
    • 实际上,我的错,在计算器中使用异步会很好,因为 hardToCalculate 方法可以并行完成。我已经替换了我的答案。
    • 感谢异步的介绍......这可能有助于我正在尝试做的事情。我不想编写所有可能在Calculator 中完成的数学运算的异步版本。关键是要让那些不需要了解异步工作原理的人尽可能地保持代码简单和易于测试。但是,与纯 Futures 相比,异步组合事物的方式绝对有一些优势——需要花一些时间。
    • 是的——真的很甜
    猜你喜欢
    • 2022-01-19
    • 1970-01-01
    • 1970-01-01
    • 2021-02-04
    • 1970-01-01
    • 1970-01-01
    • 2019-09-01
    • 2022-11-27
    • 2021-07-03
    相关资源
    最近更新 更多