MadMonty MadMonty - 2 months ago 7
Scala Question

Order of generators in Scala "for comprehension" affects answer

I'm new to Scala, and I'm working through Scala for the Impatient by Cay Horstmann. It was going well until I got to for comprehension, and came up against this slightly cryptic passage (section 2.6, Advanced for Loops and for Comprehensions):

[Start quote]

The generated collection is compatible with the first generator.

for (c <- "Hello"; i <- 0 to 1) yield (c + i).toChar
// Yields "HIeflmlmop"
for (i <- 0 to 1; c <- "Hello") yield (c + i).toChar
// Yields Vector('H', 'e', 'l', 'l', 'o', 'I', 'f', 'm', 'm', 'p')


[End quote]

Indeed, running this in the REPL, I see that the result of the first code snippet has type String, and the second code snippet has type scala.collection.immutable.IndexedSeq[Char].

Why do the types differ? I think I understand the second line of code, but I don't understand why the first line doesn't also have type scala.collection.immutable.IndexedSeq[Char]. What magic is happening to get a String rather than a Vector? What does the author mean by "compatible with the first generator"?

Answer

Both flatMap and map are trying to build an object of the same type if possible. The first example is effectively:

"Hello".flatMap { c => (0 to 1).map { i => (c + i).toChar } }

since you are calling String#flatMap (StringOps#flatMap to be exact), it will try to build String, and it's possible because internal collection returns a collection of Chars (try to remove toChar and you will see something very different).

In the second example:

(0 to 1).flatMap { i => "Hello".map { c =>  yield (c + i).toChar }}

it's impossible to generate a valid Range, so Range#flatMap falls back to Vector.

Another interesting example:

Map(1 -> 2, 3 -> 4).map(_._1) //> List(1, 3)

Normally Map#map will try to generate Map, but since we don't have pairs it's impossible, so it falls back to List

UPDATE

You can even use this trick if you want to generate something different from the default (e.g. I want to generate a list of Chars instead):

for { 
  _ <- List(None) // List type doesn't matter
  c <- "Hello"
  i <- 0 to 1
} yield (c + i).toChar //> List(H, I, e, f, l, m, l, m, o, p)