Scala For Comprehension Tricks

For comprehensions are beautiful.

Basic syntax & rules:

  • backwards arrows call flatmap
  • equal sign is basic assignment(you do not use val or var because everything is a val inside a for comprehension)
  • you can filter using an if at the end of the line like python list comprehensions
  • the yield at the end calls map

Simple example, let’s say we get a epoch string and we want to turn it into a date.

val epoch = "123123123"
for {
epochInt <- Try(epoch.toInt)
time <- Try(Datetime.fromEpoch(epochInt))
} yield time
this is equivalent toTry(epoch).flatMap { epochInt =>
Try(Datetime.fromEpoch(epochInt)).map { time =>
time
}
}

Every <- must be called on the same monad. The underlying type does not matter

for {
number <- Option(2)
string <- Try(number.toLong)
} yield string
// Very bad >:( because you can't mix option and try

Fortunately Try and Option are easily interchangeable

for {
number <- Option(2)
string <- Try(number.toLong).toOption
} yield string

This leads me to my next point which is that it is usually trivial to make conversions between stuff like Option, Try, Either, etc. For example, Either to Option would simply be.

implicit class EitherUtil[L,R](either: Either[L,R]) {
def toOption =
either match {
case Left(x) => None
case Right(x) => Some(x)
}
}
// The downside is that you often lose type information when // converting one to another. None is not very good for debugging

However, sometimes it gets tricky when you want to mix in stuff like Seq or Future. Let’s say you want take a Option[Int] and split it into its digits. As in Some(123) -> Seq(1,2,3). You would have to first map into the Option, turn it into a string, then map on the character like such.

val optNumber = Some(123)
for {
number <- optNumber
char <- number.toString.split("")
digit <- Try(char.toInt).toOption
} yield digit
// BAD

But you just violated the no mixing rule by flatmapping on Seq and Option so usually what you would do here is collect the second and third flatmaps together like such.

val optNumber = Some(123)
def numberToDigit: Int => Option[Seq[Int]] = Some(_.toString.split("").flatMap(Try(_.toInt).toOption))
for {
number <- optNumber
digits <- numberToDigit(number)
} yield digits

Ok, that was messy, but the idea is that if your types look something like this: Option -> List -> Option -> Future -> Option, anything downstream of the List type is unusable in the for comprehension because you can’t flatmap on the list. This will force you to move some logic around and play with the types, but it will produce very readable code. Eventually, what we really want is code that looks like this.

// F[_] is a monad like Option, Try, etc
private def doA(F[A] input): F[B] = something
private def doB(F[B] input): F[C] = something
private def doC(F[C] input): F[D] = something
def magic(input: A): F[D] = {
for {
a <- doA(input)
b <- doB(a)
c <- doC(b)
} yield c
}

However, there is one special case where you have something that looks like Future -> Option -> Future -> Option -> Future -> Option. This happens all the time when you are trying to chain network calls asynchronously. There is this really neat type called the OptionT that lets you collapse the above into a 5 line for comprehension. I wrote a little on it too here https://medium.com/@scalaisfun/optiont-and-eithert-in-scala-90241aba1bb7.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store