Skip to content

Instantly share code, notes, and snippets.

@sam
Created April 22, 2014 20:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sam/11193165 to your computer and use it in GitHub Desktop.
Save sam/11193165 to your computer and use it in GitHub Desktop.
Demonstrating different techniques for handling lots of Futures.
object HomepagePresenter {
import newsroom.api
import TupleFutureUnzip._
def apply(context: Context)(implicit ec: ExecutionContext): Future[HomepagePresenter] = {
// prepare all our futures since a for-comprehension would serialize our work (and that would be bad).
val future_Tree = ChanneledPresenter.tree
val future_Bulletin = api.bulletins.current
val future_Releases = api.releases.latest(20)
val future_Photos = api.photos.latest(20)
val future_PhotoCount = api.photos.count
val future_Videos = api.videos.latest(20)
val future_VideosCount = api.videos.count
// use the for-comprehension to flatMap our Futures for great justice.
for {
tree <- future_Tree
bulletin <- future_Bulletin
releases <- future_Releases
photos <- future_Photos
photoCount <- future_PhotoCount
videos <- future_Videos
videoCount <- future_VideosCount
} yield new HomepagePresenter(context, tree, bulletin.map(_.html), releases, photos, photoCount, videos, videoCount)
// Here's an alternative without for-comprehensions:
future_Tree flatMap { tree =>
future_Bulletin flatMap { bulletin =>
future_Releases flatMap { releases =>
future_Photos flatMap { photos =>
future_PhotoCount flatMap { photoCount =>
future_Videos flatMap { videos =>
future_VideosCount map { videoCount =>
new HomepagePresenter(context, tree, bulletin.map(_.html), releases, photos, photoCount, videos, videoCount)
}
}
}
}
}
}
}
// That's obviously terrible.
//
// But since none of these depend on each other, and we want to let them execute in parallel,
// mapping them when everything's ready, we can throw away all the future vals above and just zip them instead:
ChanneledPresenter.tree zip
api.bulletins.current.map(_.map(_.html)) zip
api.releases.latest(20) zip
api.photos.latest(20) zip
api.photos.count zip
api.videos.latest(20) zip
api.videos.count map {
case ((((((tree, bulletin), releases), photos), photoCount), videos), videoCount) =>
new HomepagePresenter(context, tree, bulletin, releases, photos, photoCount, videos, videoCount)
}
// Ok. That's better. But that pattern match is pretty gnarly. Implicit extension methods to the rescue!
// implicit class Tuple8Unzip[A,B,C,D,E,F,G,H](val self: Tuple8Futures[A,B,C,D,E,F,G,H]) extends AnyVal {
// def unzip[R](fun: Function8[A,B,C,D,E,F,G,H,R])(implicit ec: ExecutionContext): Future[R] = {
// self._1 zip self._2 zip self._3 zip self._4 zip self._5 zip self._6 zip self._7 zip self._8 map {
// case (((((((a, b), c), d), e), f), g), h) => fun(a,b,c,d,e,f,g,h)
// }
// }
// }
// Now you can just pass "unzip" a matching function. In this case it'll just be one we lifted from
// the constructor.
(ChanneledPresenter.tree,
api.bulletins.current.map(_.map(_.html)), // Future[Option[Bulletin]]
api.releases.latest(20),
api.photos.latest(20),
api.photos.count,
api.videos.latest(20),
api.videos.count) unzip { new HomepagePresenter(context, _, _, _, _, _, _, _) }
// We went from 17 lines of code with the for-comprehension, with a ton of boiler-plate. To 7.
// On top of that our code is now much less fragile. It's much less likely you'll accidentally
// serialize code that could've been parallel because the new-hire called the future inside
// the for-comprehension. I also personally find this a ton more readable.
// nb: If you *do* have dependencies, and need some operations to serialize, for-comprehensions
// can be a perfectly great way to do that! Mix and match! Make pretty!
// But wait! There's more! What does it look like with Scala's new Async/Await functionality?
import scala.async.Async._
async {
new HomepagePresenter(context,
await(ChanneledPresenter.tree),
await(api.bulletins.current).map(_.html),
await(api.releases.latest(20)),
await(api.photos.latest(20)),
await(api.photos.count),
await(api.videos.latest(20)),
await(api.videos.count))
}
// In this case, I don't really see that as much of an improvement. Other cases definitely.
// One advantage is that the await values are lifted to fields in the anonymous class
// generated by the async block, so you don't need to worry about dependencies or accidentally
// serializing your flow. So other than sprinkling about an await keyword here and there,
// you don't need to treat code dealing with a lot of futures a whole lot differently than
// any other code. Which can definitely help readability vs map/flatMapping everywhere.
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment