Bottom of the Rabbit Hole: for-comprehensions and monads

case class Contact(id: Int, name: String, phone: Option[String], 
email: Option[String], friendIds: List[Int])
def dbLookup(id: Int): Try[Contact] = ???
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
}
def inviteFriends: List[Int] =
friendIds
.flatMap(id => dbLookup(id).toOption
.flatMap(contact => contact.email)
.filter(sendEmail).map(_ => id))

Introducing for-comprehensions

Scala has an interesting thing called a for-comprehension. What’s interesting is that it’s one of these things we call “syntactic sugar”, which means it’s not actually a new language function, but rather a convenient way of writing things that gets rewritten behind the scenes behind the compiler.

for (id <- friendIds) {
???
}
def inviteFriends: List[Int] = {

val successes = for {
id <- friendIds
contact <- dbLookup(id).toOption
emailAddr <- contact.email
if sendEmail(email)
} yield(id)

successes
}
def inviteFriends: List[Int] = {
for {
id <- friendIds
contact <- dbLookup(id).toOption
emailAddr <- contact.email
if sendEmail(emailAddr)
} yield(id)
}
  • We iterate through the friendIds, calling each one id.
  • We call dbLookup on the id, and convert its resulting Try into an Option, assigning it’s wrapped value to something named contact. (In other words, if the dbLookup doesn’t throw an error, its returned Contact object will be assigned to contact.
  • If the optionalcontact.email field has a value, it will be assigned to emailAddr.
  • And if sendEmail returns true…
  • We’ll yield the id from the top of our loop logic.
val successes = friendIds
.flatMap(id => dbLookup(id).toOption
.flatMap(contact => contact.email)
.filter(sendEmail).map(_ => id))
val successes = friendIds
.flatMap(id => dbLookup(id).toOption
.map(contact => contact.email)
.filter(sendEmail).map(_ => id))
def inviteFriends: List[Int] = {
for {
id <- friendIds
contact <- dbLookup(id).toOption
emailAddr = contact.email
if sendEmail(emailAddr)
} yield(id)
}
  • id <- friendIds rewrites to a flatMap
  • emailAddr = contact.email rewrites to a map
  • if sendEmail(emailAddr) rewrites to a filter—not the lack of parenthesis around the condition
  • And finally, for yield(???) the contents of the yield is a function is put into a final .map operation

Unveiling my Evil Master Plan

  • A monad is a kind of typeclass that “boxes” any other type.
  • It has .map that applies functions to the contents of the boxes.
  • It has .flatMap for functions that create their own boxes in order to avoid nesting problems.
  • Option, Try, Either and even Future can be considered good examples of monads. (For the latter, let’s not talk about referential transparency, please.)
  • They isolate “pure functions” from problematic contexts like asynchronous operations, exception handling, missing data, etc.

--

--

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

82 Followers

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