gknauth
gknauth

Reputation: 2390

Can I use Action.async with multiple Futures?

In a previous SO question, I got advice on using Scala Futures with PlayFramework, thank you. Now things have gotten a bit more complicated. Let's say that before I just had to map where fruit could be found:

def getMapData(coll: MongoCollection[Document], s: String): Future[Seq[Document]] = ...

def mapFruit(collection: MongoCollection[Document]) = Action.async {
  val fut = getMapData(collection, "fruit")
  fut.map { docs: Seq[Document] =>
    Ok(docs.toJson)
  } recover {
    case e => Console.err.println("FAIL: " + e.getMessage); BadRequest("FAIL")
  }
}

It turns out that people care more about Apples than Bananas or Cherries, so if no more than 100 items should appear on the map, people want Apples to have priority over Bananas and Cherries, but not more than some percentage of items on a map should be Apples. Some function pickDocs determines the proper mix. I thought something like this might just work, but no:

def mapApplesBananasCherries(collection: MongoCollection[Document]) = Action.async {
  val futA = getMapData(collection, "apples")
  val futB = getMapData(collection, "bananas")
  val futC = getMapData(collection, "cherries")
  futA.map { docsA: Seq[Document] =>
    futB.map { docsB: Seq[Document] =>
      futC.map { docsC: Seq[Document] =>
        val docsPicked = pickDocs(100, docsA, docsB, docsC)
        Ok(docsPicked.toJson)
      }
    }
    // won't compile without something here, e.g. Ok("whatever")
  } recover {
    case e => Console.err.println("FAIL: " + e.getMessage); BadRequest("FAIL")
  }
}

Life was simple when I just had one Future, but now I have three. What can I do to make this to (1) work and (2) again be simple? I can't really construct a web response until all three Futures have values.

Upvotes: 1

Views: 381

Answers (4)

pagoda_5b
pagoda_5b

Reputation: 7373

This is a very common pattern for Futures and similar classes that "contain values" (e.g. Option, List)

To combine the results you want to use the flatMap method and the resulting code is

def mapApplesBananasCherries(collection: MongoCollection[Document]) = Action.async {
  val futA = getMapData(collection, "apples")
  val futB = getMapData(collection, "bananas")
  val futC = getMapData(collection, "cherries")
  futA.flatMap { docsA =>
    futB.flatMap { docsB =>
      futC.map { docsC =>
        val docsPicked = pickDocs(100, docsA, docsB, docsC)
        Ok(docsPicked.toJson)
      }
    }
  } recover {
    case e => Console.err.println("FAIL: " + e.getMessage); BadRequest("FAIL")
  }
}

In fact it's so common that a special syntax exists to make it more readable, called for-comprehension: the following code is equivalent to the previous snippet

def mapApplesBananasCherries(collection: MongoCollection[Document]) = Action.async {
  val futA = getMapData(collection, "apples")
  val futB = getMapData(collection, "bananas")
  val futC = getMapData(collection, "cherries")
  for {
    apples <- futA
    bananas <- futB
    cherries <- futC
  } yield {
    val docsPicked = pickDocs(100, apples, bananas, cherries)
    Ok(docsPicked.toJson)
  } recover {
    case e => Console.err.println("FAIL: " + e.getMessage); BadRequest("FAIL")
  }
}

Upvotes: 1

lyjackal
lyjackal

Reputation: 3984

This doesn't compile because your nested future block is returning a Future[Future[Future[Response]]]. If you instead use flatMap on the futures, Your futures will not be nested.

If you want this to be a little less repetitive, you can use Future.sequence instead to kick off futures simultaneously. You can either use pattern matching to re-extract the lists:

val futureCollections = List("apples", "bananas", "cherries").map{ getMapData(collection, _) }

Future.sequence(futureCollections) map { case docsA :: docsB :: docsC :: Nil =>
  Ok(pickDocs(100, docsA, docsB, docsC).toJson)
} recover {
  case e => Console.err.println("FAIL: " + e.getMessage); BadRequest("FAIL")
}

or you could just hand the pickDocs function a list of lists (sorted by priority) for it to pick from.

Future.sequence(futureCollections) map { docLists =>
  Ok(pickDocs(docLists, 100, 0.75f).toJson)
} recover {
  case e => Console.err.println("FAIL: " + e.getMessage); BadRequest("FAIL")
}

This pickDocs implementation will take a percentage of the head of the list, unless there aren't enough documents in the full list, in which it takes more, then recursively apply the same percentage on the remaining slots lists.


def pickDocs[T](lists: List[List[T]], max: Int, dampPercentage: Float): List[T] = {
  lists match {
    case Nil => Nil
    case head :: tail =>
      val remainingLength = tail.flatten.length
      val x = max - remainingLength
      val y = math.ceil(max * dampPercentage).toInt
      val fromHere = head.take(x max y)
      fromHere ++ pickDocs(tail, max - fromHere.length, dampPercentage)
  }
}

Upvotes: 1

Luis Ramirez-Monterosa
Luis Ramirez-Monterosa

Reputation: 2242

If my understanding is that you want to execute apples, cherries and bananas in that priority, I would code it similar to this

import scala.concurrent.{Await, Future}
import scala.util.Random
import scala.concurrent.duration._

object WaitingFutures extends App {

  implicit val ec = scala.concurrent.ExecutionContext.Implicits.global

  val apples = Future {50 + Random.nextInt(100)}
  val cherries = Future {50 + Random.nextInt(100)}
  val bananas =  Future {50 + Random.nextInt(100)}

  val mix = for {
    app <- apples
    cher <- if (app < 100) cherries else Future {0}
    ban <- if (app + cher < 100) bananas else Future {0}
  } yield (app,cher,ban)


  mix.onComplete {m =>
    println(s"mix ${m.get}")
  }

  Await.result(mix, 3 seconds)

}

if apples returns more than 100 when the future completes, it doesn't wait until cherries or bananas are done, but returns a dummy future with 0. If it's not enough it will wait until cherries are executed and so on.

NB I didn't put much effort on how to signal the if, so I'm using the dummy future which might not be the best approach.

Upvotes: 2

Igor Mielientiev
Igor Mielientiev

Reputation: 586

Basically, you should use flatMap

futA.flatMap { docsA: Seq[String] =>
  futB.flatMap { docsB: Seq[String] =>
    futC.map { docsC: Seq[String] =>
      docsPicked = pickDocs(100, docsA, docsB, docsC)
        Ok(docsPicked.toJson)
      }
    }
}

Also, you can use for comprehension:

val res = for {
  docsA <- futA
  docsB <- futB
  docsC <- futC
} yield Ok(pickDocs(100, docsA, docsB, docsC).toJson)
res.recover {
  case e => Console.err.println("FAIL: " + e.getMessage); BadRequest("FAIL")
}

Upvotes: 2

Related Questions