Last active
October 18, 2022 14:59
-
-
Save beala/959f507ed15955f7b7f1 to your computer and use it in GitHub Desktop.
Mixing side effects and futures is painful.
This file contains 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
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