Grocking for loops

I rarely use for loops because I can do what I need with map and flatMap, but I need to learn them as they are part of the language, and a good scala programmer is expected to understand them.

Can someone tell me how to convert the following to a for loop? I.e., how to bind variables such as v1, and v2 before after the c2 <- cards line in the for body. If I use v1 <- complete(c1) then it will treat the value like a monad and flatMap over it. Do I need to artificially wrap it in an Option?

      cards.flatMap { c1 => {
        val v1 = complete(c1)
        cards.flatMap { c2 => {
          val v2 = complete(c2)
          if (test(v1, v2))
            List((c1, c2))
          else
            Nil
        }
        }
      }
      }
for {
  c1 <- cards
  v1 = complete(c1)
  c2 <- cards
  v2 = complete(c2)
  c <- List(c1, c2) if test(v1, v2)
} yield c
1 Like

The “for loop” version makes a lot more sense to me than the map/flatmap version.

1 Like

Your code could be simplified. Instead of flatMapping to a list of one element, you can map. ie., seq.flatMap(e => List(f(e))) can become seq.map(f). Below is one way to simplify your code:

cards.flatMap { c1 =>
  cards.filter(c2 => test(complete(c1), complete(c2)))
    .map(c1 -> _)
}

You could switch up the order of map and filter, or use withFilter/views for efficiency.

The for comprehension posted by @martijnhoekstra can be similarly simplified:

for {
  c1 <- cards
  c2 <- cards if test(complete(c1), complete(c2)) 
} yield (c1, c2)

The first few generators (<-) are translated into flatMap calls, but the last one always gets translated into map. The if guard in this comprehension gets translated into withFilter.

Hi stew, your version calls complete(c1) n^2 times whereas mine calls it only n times.

That’s not true. Your original also calls complete n^2 times via the inner flatMap. For each of the n values of c1, you call cards.flatMap, which then calls complete on the n values for c2 in val v2 = complete(c2). This inner call is executed n^2 times.

If you want to minimize calls of complete, you could map the cards into a tuple of the original and complete. cards.map(c => (c, complete(c))):

val completeCached = cards.map(c => (c, complete(c))
completeCached.flatMap { case(c1, v1) =>
  completeCached
    .filter { case (_, v2) => test(v1, v2) }
    .map { case (c2, _) => (c1, c2) }
}

Alternatively, you could combine the filter and map into a collect, or memoize with a Map to avoid the tuple matching. As a for comprehension, this looks like:

val completeCached = cards.map(c => (c, complete(c))
for {
  (c1, v1) <- completedCached
  (c2, v2) <- completedCached if test(v1, v2)
} yield (c1, c2)
1 Like

The bettet approach is to use the built in lazy caching of Stream IMO

val str = cards.toStream.map(c => (c, f(c))

for {
  (c1, t1) <- str
  (c2, t2) <- str if pred(t1, t2)
} yield (c1, c2)

For minimal unneedeed reevaluation

2 Likes

If you’re going to consume the whole stream right away, why bother with the overhead?

@stewSquared, yes you are right. I am writing c1 / cards / complete(c1) and c2 / cards / complete(c2) but I’m really thinking c1 / cards1:T1 / complete1(c1) and c2 / cards2:T2 / complete2(c2). Its just that the silly original example omitted those details for brevity. In my mind I was trying to minimize the number of calls to complete1(c1)

However, if I’m not mistaken, your point was that I can translate my flatMap/if List else Nil idiom into a filter/map idiom. Or did I misunderstand your point. If so, is the latter idiom really better than the first? Is it a question of computation or just readability?

1 Like

That depends on what you do with the result. If you want, for example, to fetch the first result from it, you don’t need to evaluate the entire stream.

1 Like

Mostly it’s a question of common idiom. Nesting Monads as flatMap/flatMap/…/map is really common, and is the typical way of thinking about it – which is why it’s precisely what a for comprehension desugars to. Your approach is totally legal, and functionally equivalent, but I’d say Stew’s is more idiomatic Scala.

(Side-note: I recommend getting out of the habit of thinking of them as “for loops”, especially if you’re teaching this stuff. Many central use cases of for have nothing to do with looping, so it’s a bit misleading. “Comprehension” is a mouthful, but I’ve usually found it better in the long run.)

1 Like

Yep! That’s my main point. The final generator of a for comprehension always translates to a map rather than a flatMap. It’s more idiomatic, and it saves on allocation of an extra List layer that will just be flattened.