Skip to content

Instantly share code, notes, and snippets.

@beala
Last active October 18, 2022 14:59
Show Gist options
  • Save beala/959f507ed15955f7b7f1 to your computer and use it in GitHub Desktop.
Save beala/959f507ed15955f7b7f1 to your computer and use it in GitHub Desktop.
Mixing side effects and futures is painful.
import com.twitter.util._
// import scala.concurrent.Future
// import scala.concurrent.ExecutionContext.Implicits.global
// Prints 'future' once.
Future{println("future")}
// Lets give that action a name
lazy val printLoop = Future{println("future")}
// Now lets evaluate it.
printLoop // Prints 'future'
printLoop // Prints nothing.
// So side effects in Futures aren't referentially transparent.
// Maybe unsurprising, but this bites us in weird ways
// Now (not remembering that side-effecting Futures aren't RT) I try to print
// 'future' over and over again. Let's start with a fresh printLoop action.
lazy val printLoop2 = Future{println("future")}
// Rather than printing "future" forever, this prints it once and stops.
printLoop2.flatMap(_ => printLoop2)
// Well that's not what I wanted. But what happens if I give this
// future.flatMap(_=>future) pattern a name? We get different behavior!
def repeat1[A](f: Future[A]): Future[A] = f.flatMap(_ => repeat1(f))
lazy val printLoop3 = Future{println("future")}
// This prints "future" once and hangs. We enter a loop, but the side-effect only occurs once.
repeat1(printLoop3)
// And we get yet different results if we make `repeat1` pass-by-name
def repeat2[A](f: => Future[A]): Future[A] = f.flatMap(_ => repeat2(f))
lazy val printLoop4 = Future{println("future")}
// Finally this is what I wanted. This prints "future" over-and-over forever.
repeat2(printLoop4)
// But this doesn't fix the issue for Future actions that have already been exhausted.
// This hangs and doens't print anything.
lazy val printLoop5 = Future{println("future")}
printLoop5 // Exhaust the future.
repeat2(printLoop5) // Hangs.
// "Well don't put side effects in Futures. What else do you expect by pain?" you say.
//
// I don't disagree, but they make the following common patterns dangerous:
//
// 1. Making a long running side-effecting function async by wrapping it in a Future.
//
// def longRunning(): Unit = ???
// def longRunningAsync: Future[Unit] = Future(longRunning()) // Might need FuturePool if
// // you're using Twitter Futures.
//
// longRunningAsync works, until you try to layer any abstraction on top if it.
//
// 2. Inter-process communication. Some APIs use Futures as a way of signalling across threads.
// eg, there might be a `write(a: A): Future[Unit]`, which mutates something and returns a
// Future[Unit] that doesn't resolve until someone has called `read(): Future[A]`.
// Now layering anything on top of `write` requires us to think carefully about whether or
// not its side-effect will actually occur. This results in despair.
// See: https://twitter.github.io/util/docs/index.html#com.twitter.io.Reader$$Writable
//
// 3. Let's face it: as long as Scala has no batteries-included way of describing effects in the
// 'real world' in a RT way (eg, Haskell's IO type), people will continue to dangerously
// side-effect in their Futures. So while I can avoid putting side-effects in my Futures (which
// requires all sorts of contortions), I still must interface with code that doesn't.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment