Diving deeper into Scala’s Option, Try & Either

case class Contact(id: Int, name: String, phone: Option[String],
email: Option[String], friendIds: List[Int])
def dbLookup(id: Int): Try[Contact]
def sendEmail(email: String): Boolean
  • The database lookup may not be successful (id not present or other technical problems) handled by Try.
  • Once we look up a contact, the record may not have an email address, handled by Option.
  • The email delivery system may fail, and it only reports this by returning false.

The Imperative Approach

def inviteFriends: List[Int] = {

var successes: List[Int] = List.empty

for (id <- friendIds) {
val record = dbLookup(id)
if (record.isSuccess) {
val maybeEmail = record.get.email
if (maybeEmail.isDefined) {
val send = sendEmail(maybeEmail.get)
if (send) successes = id :: successes
}
}
}

successes
}
  • No mutable state—i.e. no vars.
  • No looping (BTW: if you really want to do this broadly, you’ll need to get comfortable with recursion, and for large collections, you’ll have to understand when and how to do tail recursion.)
def inviteFriends: List[Int] = {
val successes = friendIds.map { id =>
dbLookup(id) match {
case Failure(exception) => None
case Success(friend) => {
friend.email match {
case Some(email) => if (sendEmail(email)) Some(id) else None
case None => None
}
}
}
}
successes.filter(_.isDefined).map(_.get)
}

Happy and Unhappy Paths

Map to the Rescue

def inviteFriends: List[Int] = {

val successes = friendIds.map { id =>
dbLookup(id).map { contact =>
contact.email.map { address =>
sendEmail(address)
}
}
}

??? // successes is now of type List[Try[Option[Boolean]]]
}
def inviteFriends: List[Int] = {

val successes = friendIds.map { id =>
dbLookup(id).map { contact =>
contact.email.map { address =>
if (sendEmail(address)) Some(id) else None
}
}
}

??? // successes is now of type List[Try[Option[Option[Int]]]]
}
def inviteFriends: List[Int] = {

val successes = friendIds.map { id =>
dbLookup(id).map { contact =>
contact.email.map { address =>
if (sendEmail(address)) Some(id) else None
}
}
}

success.filter(s => s.isSuccess && s.get.isDefined &&
s.get.get.isDefined)
.map(_.get.get.get)
}

Boxes within Boxes (“It’s turtles all the way down!”)

Each of these hold a value of type T in the happy path.

FlatMap to the Rescue!

Warning: Only use flatMap for the same kinds of boxes!

Almost Done!

def inviteFriends: List[Int] = {

val successes = friendIds
.flatMap(id => dbLookup(id).toOption
.flatMap(contact => contact.email)
.filter(sendEmail).map(_ => id))

successes
}

--

--

--

Life-long learner, foodie and wine enthusiast, living in Austin, Texas.

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Load testing — You can pay attention to this | Agilitest blog

SOLID principles: The Definitive Guide (Part I)

How to create Rest API using AWS Lambda function and connect On-Premise ORACLE Database

Get Scraping With Scrapy

ReactiveConf 2019: Europe’s one-of-a-kind tech festival

It’s not Tech Debt, it’s a Boat Leak and You’re Sinking.

Axure Tutorial: Left Navigation Adapt to the Height of the Browser Window

Intro to Routing Constraints

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
Murray Todd Williams

Murray Todd Williams

Life-long learner, foodie and wine enthusiast, living in Austin, Texas.

More from Medium

Scala 3+ with Akka is it really possible?

Understanding the “Found ZIO.Task Required ZIO.ZIO” dilemma

Telemetry with Scala, part 1: OpenTelemetry

Safer Exceptions in Scala 3