Last active
November 26, 2021 15:36
-
-
Save eamelink/15d7d70f5fe9bd67eef5 to your computer and use it in GitHub Desktop.
Working with Eithers in Futures
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 scala.concurrent.Future | |
object either { | |
// Scala standard library Either is sometimes used to distinguish between 'failure' and 'success' state. Like these two methods: | |
def getUser(id: String): Either[String, User] = ??? | |
def getPreferences(user: User): Either[String, Preferences] = ??? | |
// The Right side contains the success value by convention, because right is right, right? | |
// We can compose these together. Now if either the user can't be found, or his preferences can't be found, | |
// This will be a `Left` containing a String error message. | |
val sendNewsLetter: Either[String, Boolean] = getUser("foo").right.flatMap { getPreferences(_) }.right.map { _.newsLetter } | |
// But, Either is not great at all for this, because it's not a monad. It doesn't have `map` or `flatMap`, only the projections | |
// (that you get with '.right' or '.left') do. Either is _unbiased_ and it doesn't care about our convention to put the success | |
// value on the right. | |
// It's not a monad, so we can't use it in a for comprehension: | |
/* Doesn't compile | |
for { | |
user <- getUser("123") | |
prefs <- getPreferences(user) | |
} yield prefs.newsLetter | |
*/ | |
// In some cases, it works if you add '.right' strategically: | |
for { | |
user <- getUser("123").right | |
prefs <- getPreferences(user).right | |
} yield prefs.newsLetter | |
// But many other variants will desugar to something invalid, like this: | |
/* Doesn't compile | |
for { | |
user <- getUser("123").right | |
prefs <- getPreferences(user).right | |
newsLetter = prefs.newsLetter | |
} yield newsLetter | |
*/ | |
// This desugars to something with two consecutive .map or .flatMap without a possibility for us to inject a `.right` in between. | |
} | |
object disjunction { | |
// Luckily, there is an alternative to Either, called \/. Yes, that's the classname. People pronounce it as 'Disjunction' (or sometimes | |
// 'Scalaz Either', or just plain 'Either' again). \/ has two instances: \/-, which is the right value, and -\/, which is the left value. | |
import scalaz.{ \/, \/-, -\/ } | |
// \/ works a lot like Either. So instead of Either[String, User] we could use \/[String, User]. | |
// One neat trick is to use \/ in infix notation. So instead of \/[String, User], you can use String \/ User. This cuts down on square brackets | |
// in composite types. | |
// \/ is monad! And it's right-biased, which means we can immediately map the right value without projections. | |
def getUser(id: String): String \/ User = ??? | |
def getPreferences(user: User): String \/ Preferences = ??? | |
for { | |
user <- getUser("123") | |
prefs <- getPreferences(user) | |
newsLetter = prefs.newsLetter | |
} yield newsLetter | |
// Conclusion: For biased computation, where we consider the right side to be 'success' and left 'failure', \/ gives us much better compositionality | |
// than Either and we should all be using \/. Woohoo!!! | |
} | |
object nestedcontainers { | |
import scalaz. { \/, \/-, -\/ } | |
import scala.concurrent.ExecutionContext.Implicits.global | |
// Often, we work asynchronously. So our \/ are embedded in a Future. In for-comprehensions, this means that on the left side of the arrows, | |
// we get \/'s instead of the values inside the \/'s, which is very annoying if we need those values to call the next function: | |
// Suppose this is the api we work with: | |
def getUser(id: String): Future[String \/ User] = ??? | |
def getPreferences(user: User): Future[String \/ Preferences] = ??? | |
// Now if we try to build our program, it fails: | |
/* Doesn't compile: `user` is of type `String \/ User` and `getPreferences` needs `User`. | |
for { | |
user <- getUser("123") | |
prefs <- getPreferences(user) | |
newsLetter = prefs.newsLetter | |
} yield newsLetter | |
*/ | |
// Basically, our 'User' is wrapped into two containers: first a \/, and then a Future. The for-comprehension desugars | |
// to `map` and `flatMap` on the Future, but will not get the User out of the \/. | |
// So how do we deal with this? | |
// We want to fuse these two containers together into a thing that has `map` and `flatMap` that map the inner value. It's not so hard to build one: | |
case class FutureDisjunctionFuser[F, A](inner: Future[F \/ A]) { | |
def map[B](f: A => B): FutureDisjunctionFuser[F, B] = FutureDisjunctionFuser { | |
inner.map { _.map(f) } | |
} | |
def flatMap[B](f: A => FutureDisjunctionFuser[F, B]): FutureDisjunctionFuser[F, B] = FutureDisjunctionFuser { | |
inner.flatMap { | |
case -\/(failure) => Future.successful(-\/(failure)) | |
case \/-(a) => f(a).inner | |
} | |
} | |
} | |
// Now if we use the, we can use the for-comprehension again: | |
val result = for { | |
user <- FutureDisjunctionFuser(getUser("123")) | |
prefs <- FutureDisjunctionFuser(getPreferences(user)) | |
newsLetter = prefs.newsLetter | |
} yield newsLetter | |
// result is now of type `FutureDisjunctionFuture[String, Boolean]`. We can use '.inner' to get back to the regular structure of 'Future[String \/ Boolean]`: | |
val out = result.inner | |
// Note that the `map` and `flatMap` only do something if the future is successful and the \/ is a \/- (right). This means that | |
// if any computation returns a failed future or a failed \/, that will be the final result of the total computation. | |
// We don't have to manually create `Fuser` classes for every combination of monads. It turns out that you can | |
// create a so called Transformer for the inner monad, and with that transform any given outer monad to a new monad | |
// that has the behaviour of both. There's more to this, ask Erik for the details if you're interested | |
// In our case, we want to transform a pair of monads with the Scalaz Either as the | |
// inner one, and we can use Scalaz's EitherT class (T stands for Transformer) to transform Future into a monad | |
// that behaves as the combination of a future and a disjunction, just like our `FutureDisjunctionFuser`. So, we can also do: | |
import scalaz.EitherT | |
import scalaz.std.scalaFuture.futureInstance | |
val result2 = for { | |
user <- EitherT(getUser("123")) | |
prefs <- EitherT(getPreferences(user)) | |
newsLetter = prefs.newsLetter | |
} yield newsLetter | |
val out2 = result2.run | |
} | |
object syntax { | |
import scalaz.EitherT | |
import scala.concurrent.ExecutionContext.Implicits.global | |
import scalaz.std.scalaFuture.futureInstance | |
import scalaz. { \/, \/-, -\/ } | |
// Often, you use the same monad stack in a part of your application. It's then often nice to give that | |
// a proper name and make the methods return the monad stack directy: | |
type Result[A] = EitherT[Future, String, A] | |
// We can make the api like this: | |
def getUser(id: String): Result[User] = ??? | |
def getPreferences(user: User): Result[Preferences] = ??? | |
// Now our final program becomes really clean again: | |
val result = for { | |
user <- getUser("123") | |
preferences <- getPreferences(user) | |
newsLetter = preferences.newsLetter | |
} yield newsLetter | |
val out: Future[String \/ Boolean] = result.run | |
// Potential next steps: | |
// - Choose a better failure type | |
// - Accumulating errors when needed | |
// Erik can tell you more about these | |
} | |
// This is just to make the whole thing compile | |
trait User | |
trait Preferences { def newsLetter: Boolean } |
I agree. Thank you very much!
How would you construct the for comprehension if your getPreferences
function just returned Future[Preferences]
:
def getPreferences(user: User): Future[Preferences] = ???
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Incredibly helpful !