Saturday 10 January 2009

Scala, for comprehensions

This post contains some examples of Scala's for comprehension.

A for loop in Scala can be written in the imperative style and will behave rather much the same as a Java for loop, especially the enhanced Java for loop from Java 5.0.

However, this type of simple imperative looping doesn't touch on the power and flexibility of Scala's for comprehension, when used as used in a functional programming context.

In particular, some of the key differences are:

  • A for comprehension can be a value itself. The value is a product of the yield statement used within the expression body. Since a for comprehension iterates over a list or sequence, the resulting value of a for comprehension is typically a list too.
  • A for comprehension uses a generator, written in the form item <- listOfItems to define the items to be iterated over
  • Multiple generators may be used, somewhat analogous to a nested for loop within a for loop in imperative programming
  • In addition to generators, a guard can be used to filter or restrict the values
  • The for syntax can use parenthesis with semicolon delimited generators and guards or braces - both are equivalent
  • The iterations my differ when compared to a seeming equivalent imperative construct, because the implementation is fundamentally different. In particular the list of items to be used is determined using the generator(s) and guard(s) before the body is run, so for example flags used in guards that are modified in the body may not produce the expected result (from an imperative perspective).
  • Generators can be "projected" using the .project function.

Examples of Scala for comprehension:


package test

object ForComprehensionTest {
def main(args : Array[String]) {

// create a couple of simple lists...
val list1 = "Apples" :: "Oranges" :: "Bananas" :: "Avocados" :: Nil
val list2 = 11 :: 22 :: 33 :: 44 :: 55 :: 66 :: Nil

// simple comprehension with one generator and guard
val res1 = for(item1 <- list1 if item1.startsWith("A"))
yield item1

assert(res1 == List("Apples", "Avocados"))
res1.foreach(println);

// more complex comprehension, two generators within {brace} syntax with two guards
val res2 = for {
item1 <- list1 if item1.startsWith("A")
item2 <- list2 if item2 % 3 == 0
}
yield item2 + " " + item1

assert(res2 == List("33 Apples", "66 Apples", "33 Avocados", "66 Avocados"))
res2.foreach(println);

// as above but using () parens and ; semi-colon syntax instead
val res3 = for ( item1 <- list1 if item1.startsWith("A");
item2 <- list2 if item2 % 3 == 0 )
yield item2 + " " + item1

assert(res3 == List("33 Apples", "66 Apples", "33 Avocados", "66 Avocados"))
res3.foreach(println);

println("simple guard item == 22")
for (item <- list2 if item == 22) println(item)

println("simple comprehension mkString = " + (for (item <- list2 if item % 2 == 0) yield item).mkString)

// from an imperative / Java perspective you might expect this to stop with item = 22
// but it continues on because !found is evaluated as always true (irrefutable pattern) when determining what to iterate over
var found = false
for (item <- list2 if !found) {

if (item == 22)
found = true

println("without projection, item == 22? item = " + item)
}

// with the .projection call, it works more like you might expect from an imperative perspective
found = false
for (item <- list2.projection if !found) {

if (item == 22)
found = true

println("with .projection, item==22? item = " + item)
}

found = false
for (item <- list2.projection.takeWhile(item => !found)) {

if (item == 22)
found = true

println(".projection.takeWhile(item => ! found) = " + item)
}
}
}


Results:


Apples
Avocados
33 Apples
66 Apples
33 Avocados
66 Avocados
33 Apples
66 Apples
33 Avocados
66 Avocados
simple guard item == 22
22
simple comprehension mkString = 224466
without projection, item == 22? item = 11
without projection, item == 22? item = 22
without projection, item == 22? item = 33
without projection, item == 22? item = 44
without projection, item == 22? item = 55
without projection, item == 22? item = 66
with .projection, item==22? item = 11
with .projection, item==22? item = 22
.projection.takeWhile(item => ! found) = 11
.projection.takeWhile(item => ! found) = 22


Other useful things to know:

In a for comprehension if you want to iterate over a collection you can create a tuple of (element, index) pairs using syntax of the form:


for ((elem, index) <- iter.zipWithIndex) {
// now we can access element of each iteration along with its associated numerical index!
}



Resources:

A good explanation of for comprehensions can be found here http://creativekarma.com/ee.php/weblog/comments/the_scala_for_comprehension_from_a_java_perspective/


.

1 comment:

Matt R said...

Interesting post. For comprehensions are something like my fourth favourite Scala feature.

Still, I feel a bit horrified at the trickiness and subtlety of the example with and without calling .projection(). A for comprehension appeals to intuitions about imperative for loops, and as a consequence it's really easy to be lulled into forgetting what it's actually being desugared to.