Created
April 22, 2014 20:29
-
-
Save sam/11193165 to your computer and use it in GitHub Desktop.
Demonstrating different techniques for handling lots of Futures.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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