如何在Scala中实现Future作为应用?

时间:2015-05-01 08:05:31

标签: scala concurrency functional-programming future applicative

假设我需要运行两个并发计算,等待它们,然后合并它们的结果。更具体地说,我需要同时运行f1: X1 => Y1f2: X2 => Y2,然后调用f: (Y1, Y2) => Y以获得Y的值。

我可以创建将来的计算 fut1: X1 => Future[Y1]fut2: X2 => Future[Y2],然后将它们组合起来以使用monadic合成来获取fut: (X1, X2) => Future[Y]

问题是monadic组合意味着顺序等待。在我们的例子中,它意味着我们先等待一个未来然后我们将等待另一个。例如。如果它需要2秒。到第一个未来完成,只需1秒。到第二个未来失败,我们浪费1秒。

因此看起来我们需要应用组成期货,等待 完成或至少一个未来失败。是否有意义 ?你如何实现<*>期货?

4 个答案:

答案 0 :(得分:5)

其他答案中的所有方法都没有做到正确的事情,以防未来快速失败,再加上未来很长时间后成功。

但是这种方法可以手动实现:

def smartSequence[A](futures: Seq[Future[A]]): Future[Seq[A]] = {
  val counter = new AtomicInteger(futures.size)
  val result = Promise[Seq[A]]()

  def attemptComplete(t: Try[A]): Unit = {
    val remaining = counter.decrementAndGet
    t match {
      // If one future fails, fail the result immediately
      case Failure(cause) => result tryFailure cause
      // If all futures have succeeded, complete successful result
      case Success(_) if remaining == 0 => 
        result tryCompleteWith Future.sequence(futures)
      case _ =>
    }
  }

  futures.foreach(_ onComplete attemptComplete)
  result.future
}

ScalaZ在内部做了类似的事情,因此f1 |@| f2List(f1, f2).sequence在任何期货失败后立即失败。

以下是对这些方法失败时间的快速测试:

import java.util.Date
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scalaz._, Scalaz._

object ReflectionTest extends App {
  def f1: Future[Unit] = Future {
    Thread.sleep(2000)
  }

  def f2: Future[Unit] = Future {
    Thread.sleep(1000)
    throw new RuntimeException("Failure")
  }

  def test(name: String)(
    f: (Future[Unit], Future[Unit]) => Future[Unit]
  ): Unit = {
    val start = new Date().getTime
    f(f1, f2).andThen {
      case _ => 
        println(s"Test $name completed in ${new Date().getTime - start}")
    }
    Thread.sleep(2200)
  }

  test("monadic") { (f1, f2) => for (v1 <- f1; v2 <- f2) yield () }

  test("zip") { (f1, f2) => (f1 zip f2).map(_ => ()) }

  test("Future.sequence") { 
    (f1, f2) => Future.sequence(Seq(f1, f2)).map(_ => ()) 
  }

  test("smartSequence") { (f1, f2) => smartSequence(Seq(f1, f2)).map(_ => ())}

  test("scalaz |@|") { (f1, f2) => (f1 |@| f2) { case _ => ()}}

  test("scalaz sequence") { (f1, f2) => List(f1, f2).sequence.map(_ => ())}

  Thread.sleep(30000)
}

我机器上的结果是:

Test monadic completed in 2281
Test zip completed in 2008
Test Future.sequence completed in 2007
Test smartSequence completed in 1005
Test scalaz |@| completed in 1003
Test scalaz sequence completed in 1005

答案 1 :(得分:2)

您的帖子似乎包含两个或多或少独立的问题。 我将首先解决运行两个并发计算的具体实际问题。最后回答了关于ng-disabled="isDisabled"的问题。

假设您有两个异步函数:

Applicative

还有两个值:

val f1: X1 => Future[Y1]
val f2: X2 => Future[Y2]

现在,您可以通过多种不同的方式开始计算。我们来看看其中一些。

val x1: X1 val x2: X2 之外开始计算(并行)

假设你这样做:

for

现在,计算val y1: Future[Y1] = f1(x1) val y2: Future[Y2] = f2(x2) f1已在运行。您收集结果的顺序无关紧要。你可以用f2 - 理解:

来做到这一点
for

使用val y: Future[(Y1,Y2)] = for(res1 <- y1; res2 <- y2) yield (res1,res2) 中的表达式y1y2 - 理解不会干扰fory1的计算顺序,它们仍然是并行计算。

y2(顺序)

内开始计算

如果我们只是简单地采用fory1的定义,并将它们直接插入到y2理解中,我们仍会得到相同的结果,但执行的顺序将是不同:

for

转换为

val y = for (res1 <- f1(x1); res2 <- f2(x2)) yield (res1, res2)
特别是,第二次计算在第一次计算终止后开始。这通常不是人们想要的。

这里违反了基本替代原则。如果没有副作用,可能会将此版本转换为前一版本,但在Scala中,必须明确地处理执行顺序。

压缩期货(平行)

期货尊重产品。有一种方法val y = f1(x1).flatMap{ res1 => f2(x2).map{ res2 => (res1, res2) } } ,允许您这样做:

Future.zip

这将同时运行两个计算,直到两个计算完成,或者直到其中一个计算失败。

<强>演示

这是一个演示此行为的小脚本(受val y = f1(x1) zip f2(x2) 帖子的启发):

muhuk

输出:

import scala.concurrent._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import java.lang.Thread.sleep
import java.lang.System.{currentTimeMillis => millis}

var time: Long = 0

val x1 = 1
val x2 = 2

// this function just waits
val f1: Int => Future[Unit] = { 
  x => Future { sleep(x * 1000) }
}

// this function waits and then prints
// elapsed time
val f2: Int => Future[Unit] = {
  x => Future { 
    sleep(x * 1000)
    val elapsed = millis() - time
    printf("Time: %1.3f seconds\n", elapsed / 1000.0)
  }
}

/* Outside `for` */ {
  time = millis()
  val y1 = f1(x1)
  val y2 = f2(x2)
  val y = for(res1 <- y1; res2 <- y2) yield (res1,res2)
  Await.result(y, Duration.Inf)
}

/* Inside `for` */ {
  time = millis()
  val y = for(res1 <- f1(x1); res2 <- f2(x2)) yield (res1, res2)
  Await.result(y, Duration.Inf)
}

/* Zip */ {
  time = millis()
  val y = f1(x1) zip f2(x2)
  Await.result(y, Duration.Inf)
}

<强>应用型

使用your other post中的此定义:

Time: 2.028 seconds
Time: 3.001 seconds
Time: 2.001 seconds

可以做这样的事情:

trait Applicative[F[_]] {
  def apply[A, B](f: F[A => B]): F[A] => F[B]
}

但是,我不确定这与您的具体问题或可理解且可读的代码有什么关系。 object FutureApplicative extends Applicative[Future] { def apply[A, B](ff: Future[A => B]): Future[A] => Future[B] = { fa => for ((f,a) <- ff zip fa) yield f(a) } } 已经 是一个monad(这比Future更强),甚至还有内置语法,所以我看不出任何优势在这里添加一些Applicative

答案 2 :(得分:2)

  

问题是monadic组合意味着顺序等待。在我们的例子中,它意味着我们先等待一个未来,然后我们将等待另一个。

不幸的是,这是真的。

import java.util.Date
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

object Test extends App {
        def timestamp(label: String): Unit = Console.println(label + ": " + new Date().getTime.toString)

        timestamp("Start")
        for {
                step1 <- Future {
                        Thread.sleep(2000)
                        timestamp("step1")
                }
                step2 <- Future {
                        Thread.sleep(1000)
                        timestamp("step2")
                }
        } yield { timestamp("Done") }

        Thread.sleep(4000)
}

运行此代码输出:

Start: 1430473518753
step1: 1430473520778
step2: 1430473521780
Done: 1430473521781
  

因此看起来我们需要期货的应用组合等到完成或至少一个未来都失败。

我不确定应用程序组成与并发策略有什么关系。使用for理解,如果所有期货都已完成,则会得到结果,如果任何期货失败,则会收到失败。所以它在语义上是一样的。

为什么他们按顺序运行

我认为期货按顺序运行的原因是因为step1step2内(以及其余计算中)可用。基本上我们可以将for块转换为:

def step1() = Future {
    Thread.sleep(2000)
    timestamp("step1")
}
def step2() = Future {
    Thread.sleep(1000)
    timestamp("step2")
}
def finalStep() = timestamp("Done")
step1().flatMap(step1 => step2()).map(finalStep())

因此,其余步骤可以使用先前计算的结果。它不同于<?>&amp; <*>就此而言。

如何并行运行期货

@ andrey-tyukin的代码并行运行期货:

import java.util.Date
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

object Test extends App {
        def timestamp(label: String): Unit = Console.println(label + ": " + new Date().getTime.toString)

        timestamp("Start")
        (Future {
                Thread.sleep(2000)
                timestamp("step1")
        } zip Future {
                Thread.sleep(1000)
                timestamp("step2")
        }).map(_ => timestamp("Done"))
        Thread.sleep(4000)
}

输出:

Start: 1430474667418
step2: 1430474668444
step1: 1430474669444
Done: 1430474669446

答案 3 :(得分:1)

它不需要是顺序的。未来的计算可能会在创建未来的那一刻开始。当然,如果未来是由flatMap参数创建的(如果它需要第一次计算的结果则必须如此),那么它将是顺序的。但是在诸如

之类的代码中
val f1 = Future {....}
val f2 = Future {....}
for (a1 <- f1; a2 <- f2) yield f(a1, a2)

你得到并发执行。

所以Monad暗示的Applicative的实现是可以的。