Вопрос

I have a scenario wherein I get a List of String messages and I have to iterate through the String and call another method which is sort of a long running process. I have to then collect the results of this long running process and concatenate the results and send it back to the user interface. I'm pretty new to these Future concepts in Scala. I'm using Play framework where-in the list of Strings will come from the user interface. Here is how my first attempt at implementing ht scenario looks:

def futuresTest(strList: List[String]) = Action {
  Async {

    val ftrList: List[Future[String]] =
      strList.map(s => Akka.future {longRunningCall(s)} ).toList

    val futureResultList = Future.sequence(ftrList)

    val jsonResponse: String =
      futureResultList.map(_.sum).asInstanceOf[String]

    Akka.future { Ok(jsonResponse) }
  }
}

For simplicity, the longRunningCall would just return a String. Later I will tie it up to the original implementation.

def longRunningCall(s: String) = "test"

My question here is that in the statement:

val ftrList: List[Future[String]] =
  strList.map(s => Akka.future {longRunningCall(s)} ).toList

I would assume that the ftrList will be populated asynchronously and when it hits the following line, I'm guaranteed that the futureResultList would contain all the elements (i.e., strList and futureResultList size would be equal?

val futureResultList = Future.sequence(ftrList)

Please advice!

Это было полезно?

Решение

I'm going to assume here that you mean for the strings to be concatenated. First some comments on the code:

  • There's no need to wrap the whole block in an Async, just the final future that you're returning.

  • The value types can all be inferred, you don't need to state them explicitly

  • mapping a List returns a List. Calling toList on the result is redundant.

  • List#sum only works on numbers, use foldLeft instead for strings.

  • A Future[String] can not be cast directly to a String via asInstanceOf. You'll be returning it as a Future anyway.

  • map and Future.sequence can be combined into a single Future.traverse operation.

And changing the code to apply these points:

def futuresTest(strList: List[String]) = Action {

  val ftrList = Future.traverse(strList) {
    s => Future( longRunningCall(s) )
  }

  // this will be a Future[String]
  val jsonResponse = ftrList map { _.foldLeft("")(_ + _) }

  Async { jsonResponse map (OK(_)) }
}

The last two lines could also be combined:

  Async {
    ftrList map { xs => OK(xs.foldLeft("")(_ + _)) }
  }

UPDATE

Here's the same thing using Future.fold, as suggested by Viktor

def futuresTest(strList: List[String]) = Action {     
  val ftrList = strList map { longRunningCall(_) } // List[Future]
  val jsonResponse = Future.fold(ftrList)("")(_ + _) // Future[String]
  Async { jsonResponse map (OK(_)) }
}

For handling failure, you want to recover the future and have it return a different response:

  Async { jsonResponse map (OK(_)) recover (InternalServerError(_.toString)) }

If you want to handle individual errors from each element, then you should look at the technique used in this answer.

Другие советы

What you are doing will work correct, as long as the longRunningCall will correctly return values. ftrList size will equal to strList size, unless exception from longRunningCall

It would seem to me that for what you want it would be much simpler here to just use Scala's Parallel collections rather than Futures. By using .par.map you can execute your long running operations on each item in a list in parallel and collect the results together.

def getResponse(strList: List[String]) = Action {

    val results: List[String] = strList.par.map(longRunningCall(_))
    val jsonResponse: String = results.mkString(",") //concatenate using ','

    Ok(jsonResponse)
}

http://docs.scala-lang.org/overviews/parallel-collections/configuration.html

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top