semvdwal semvdwal - 1 year ago 59
Scala Question

Inspection error in scala method / play framework / rest

I'm still learning scala so this might be a question with an easy answer, but I've been stuck on writing a single method over and over for almost a day, unable to get this code to compile.

I'm playing with the Play Framework and a reactive mongo template to learn how Scala and Play work.
I have a controller with a few methods, endpoints for a REST service.

The issue is about the following method, which accepts a list of json objects and updates those objects using the mongo reactive driver. The class has one member,

citiesFuture
which is of type
Future[JSONCollection]
.
The original class code which I'm adding this method to can be found here for context: CityController on github

def updateAll() = Action.async(parse.json) { request =>
Json.fromJson[List[City]](request.body) match {
case JsSuccess(givenCities, _) =>
citiesFuture onComplete[Future[Result]] { cities =>
val updateFutures: List[Future[UpdateWriteResult]] = for {
city <- givenCities
} yield cities.get.update(City.getUniqueQuery(city), Json.obj("$set" -> city))

val promise: Promise[Result] = Promise[Result] {
Future.sequence(updateFutures) onComplete[Result] {
case [email protected](_) =>
var count = 0
for {
updateWriteResult <- s.value
} yield count += updateWriteResult.n
promise success Ok(s"Updated $count cities")
case Failure(_) =>
promise success InternalServerError("Error updating cities")
}
}
promise.future
}
case JsError(errors) =>
Future.successful(BadRequest("Could not build a city from the json provided. " + Errors.show(errors)))
}
}


I've managed to get this far with alot of trial and error, but I'm starting to understand how some of the mechanics of scala and Futures work, I think :) I think I'm close, but my IDE still gives me a single Inspection error just at the single closing curly brace above the line
promise.future
.

The error reads: Expression of type Unit doesn't conform to expected type Nothing.
I've checked the expected return values for the Promise and onComplete code blocks, but I don't believe they expect Nothing as a return type.

Could somebody please explain to me what I'm missing, and also, I'm sure this can be done better, so let me know if you have any tips I can learn from!

Answer Source

You're kinda on the right track but as @cchantep said, once you're operating in Future-land, it would be very unusual to need to create your own with Promise.future.

In addition, it's actually quite unusual to see onComplete being used - idiomatic Scala generally favors the "higher-level" abstraction of mapping over Futures. I'll attempt to demonstrate how I'd write your function in a Play controller:

Firstly, the "endpoint" just takes care of one thing - interfacing with the outside world - i.e. the JSON-parsing part. If everything converts OK, it calls a private method (performUpdateAll) that actually does the work:

def updateAll() = Action.async(parse.json) { request =>
  Json.fromJson[List[City]](request.body) match {
    case JsSuccess(givenCities, _) =>
      performUpdateAll(givenCities)
    case JsError(errors) =>
      Future.successful(BadRequest("Could not build a city from the json provided. "))
   }
}

Next, we have the private function that performs the update of multiple cities. Again, trying to abide by the Single Responsibility Principle (in a functional sense - one function should do one thing), I've extracted out updateCity which knows how to update exactly one city and returns a Future[UpdateWriteResult]. A nice side-effect of this is code-reuse; you may find you'll be able to use such a function elsewhere.

private def performUpdateAll(givenCities:List[City]):Future[Result] = {

  val updateFutures = givenCities.map { city =>
    updateCity(city)
  }

  Future.sequence(updateFutures).map { listOfResults =>
    if (listOfResults.forall(_.ok)) {
      val count = listOfResults.map(_.n).sum
      Ok(s"Updated $count cities")          
    } else {
      InternalServerError("Error updating cities")
    }
  }
}

As far as I can tell, this will work in exactly the same way as you intended yours to work. But by using Future.map instead of its lower-level counterpart Future.onComplete and matching on Success and Failure you get much more succinct code where (in my opinion) it's much easier to see the intent because there's less boilerplate around it.

We still check that every update worked, with this:

if (listOfResults.forall(_.ok)) 

which I would argue reads pretty well - all the results have to be OK!

The other little trick I did to tidy up was replace your "counting" logic which used a mutable variable, with a one-liner:

var count = 0
for {
  updateWriteResult <- s.value
} yield count += updateWriteResult.n 

Becomes:

val count = listOfResults.map(_.n).sum

i.e. convert the list of results to a list of integers (the n in the UpdateWriteResult) and then use the built-in sum function available on lists to do the rest.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download