Skip to content

Instantly share code, notes, and snippets.

@eamelink
Last active November 26, 2021 15:36
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save eamelink/15d7d70f5fe9bd67eef5 to your computer and use it in GitHub Desktop.
Save eamelink/15d7d70f5fe9bd67eef5 to your computer and use it in GitHub Desktop.
Working with Eithers in Futures
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 }
@sebastianharko
Copy link

Incredibly helpful !

@mctigger
Copy link

I agree. Thank you very much!

@todor-kolev
Copy link

todor-kolev commented Dec 13, 2016

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