For comprehensions
Now that we know all about map
and flatMap
there is one more thing that’s very important to know - for comprehension, which you can think of as a “kind of” replacement for a for loop - although you will soon see it isn’t the same.
In short - a for comprehension gives us a syntactic sugar for working with large, sequenced and/or nested computations using flatMap
, map
and filter
.
Without further ado let’s first look at the below example. Let’s say we have two lists of Integers and we want to get all combinations of multiplications on their elements, so for List(1, 2)
and List(3, 4)
we want a result of List(3, 4, 6, 8)
…yikes. That doesn’t look pretty. Thankfully, a for comprehension gives us a great syntactic sugar and makes it easy to rewrite the above as:
That looks much more pleasant, right?
Expressions allowed in for comprehensions
In general a for comprehension follows this syntax:
Here is a list of allowed for comprehension expressions (sequenceOfExpressions
in above pattern):
- generators (
element <- list
), think of it as “for eachelement
oflist
” - definitions (
name = element.name
), think of it as a simple assignment, just likeval dogName = dog.name
- filters (
if (name == "Joe")
), well. Just a filter :)
yield
is our door out from a for comprehension - this is the only place where your values and expression evaluations from the for comprehension are in scope, this is a place to act on what to do with your result.
For comprehension rules
Now that we know the allowed expressions and what yield
does let’s talk about some rules. Let’s start with an important note:
Don’t try to memorise those!
It will come natural (and logical) to you with time.
Rule #1:
For comprehension must always start with a generator.
Rule #2:
A for comprehension with one generator is a simple map
:
…translates to…
Rule #3:
A for comprehension with two generators is a flatMap
followed by map
.
…translates to…
Rule #4:
A for comprehension with a generator and a filter is a withFilter
followed by a map
:
…translates to…
Rule #5:
A for comprehension without a yield
is a forEach
(watch for side effects! not recommended)
…translates to…
For comprehensions in real life
Scala for comprehensions become a life saver if you have tasks such as the one below. Given above rules just think what a flatmap-map-filter monster you’d create to compute the same without a for comprehension.
Let’s see. I’m not going to attempt to build a non-for-comprehension solution but given our rules above we can guess that it would involve:
- three nested flatMap/maps for generators
- withFilter for filter
- map for yield
In real life for comprehension is a great solution for any case where you need sequencing and orchestrating a number of operations. If your solution is a single map
or flatMap
maybe it’s not worth using a for comprehension. On the other hand if you’re working with a few sequential operations and find yourself mapping, filtering and flatmapping across types like Try
, Option
, Future
or any sequence of those types - that’s when for comprehension will shine.
Let’s list a few usages:
- making several database requests that return
Try
- making several http requests that return
Future
- computations across a number of sequences, e.g. across 4 lists
- bootstrapping the application - database, modules, http clients etc. (although you might think of using dependency injection instead!)
Summary
For comprehension is a syntactic sugar for dealing with a nested flatMap
, map
and filter
expressions.
(not so) hardcore note
If you ever heard the word monad (and I am sure you did and thought “wtf” - but don’t worry I did too) right here is the first place where I am going to mention it (but without going into details or explanation yet). In a massive oversimplification things that can be flatmapped over are monads (that means a simple Option
is a monad too - how cool to know you worked with this (not so) scary stuff already!).
So your (not so) hardcore addition to summary is:
For comprehensions are great for working with and composing monads.