1

If I wanted to enumerate the iterations of an inner loop in Scala, how would I approach this in a functional style?

E.g. how would I rewrite the following code:

val documents = List("a" :: "b" :: Nil, "aa" :: "bb" :: Nil, "aaa" :: Nil)
var currentPageNumber = 0

documents.foreach { doc =>
  for (page <- doc) {
    currentPageNumber += 1
    println("%d: %s".format(currentPageNumber.head, page)
    // do something with page
  }
}

I could get rid of the var by using val currentPageNumber = Iterator.from(0) but this still means that I actually need to declare it outside of the loop.

Is there a trick which would not expose currentPageNumber to the outer scope and – just like the zipWithIndex counter – only exists inside the loop?

Edit:

I’ve also found a version using scanLeft but I think it’s rather confusing. But maybe someone can optimise it somehow.

documents.scanLeft(0) { case (cnt, doc) =>
  doc.zip(Iterator.from(cnt + 1).toIterable).map { case(page, cnt) =>
    println("%d: %s".format(cnt, page))
    cnt
  } last
}

Own solution (for now):

Thanks everyone, I think what I had in mind was something like

documents.flatMap{ doc =>
  for (page <- doc) yield {
    currentPageNumber: Int =>
      "%d: %s".format(currentPageNumber, page)
}}.zipWithIndex.map (t => t._1(t._2 + 1)) map(println)

so, the trick is to leave currentPageNumber undecided until one can actually apply zipWithIndex.

4 Answers 4

6

I think that the imperative version you're using is probably better, because the printf has side effects anyway, but if you want something functional, you can do this:

documents.foldLeft(1){ (docStartingPage,doc) =>
   doc.foldLeft(docStartingPage){ (currentPageNumber, page) =>
      printf("%d: %s\n", currentPageNumber, page)
      currentPageNumber + 1
   }
}

Unfortunately, the number has to be available to the outer loop if it's going to appear correctly in the inner loop again.

You might also try the following, though you'll lose any information about where documents start and end.

for ( (page,pageNumber) <- documents.flatten.zipWithIndex )
  printf("%d: %s\n", pageNumber + 1, page)
Sign up to request clarification or add additional context in comments.

2 Comments

The flattening would not really do because in fact it’s not a list of lists.
@Debilski: if you have an implicit (or explicit) conversion from documents to Traversables, then you can use flatten.
4

You mention you cannot flatten because it is not a list of list. However, if they support flatMap and map, then you can flatten. For instance:

documents.view.flatMap(_ map identity).            // flatten
zipWithIndex.                                      // get the index
map(t => t._2 + 1 -> t._1).                        // adjust page number
map(("%d: %s".format(_: Int, _: String)).tupled).  // format output
foreach(println)                                   // tah dah

Steps three and four can be joined into a single step. Actually, you could add step five there too, but this is a view we are talking about -- so I don't think it matters much.

If, however, you are limited to foreach... well, then it's still a Traversable. You can do a .toStream and get all of the above (and remove the view):

documents.toStream.flatMap(_.toStream map identity).
zipWithIndex.                                     
map(t => t._2 + 1 -> t._1).                       
map(("%d: %s".format(_: Int, _: String)).tupled). 
foreach(println)                                  

1 Comment

You’re right. I actually can use flatten… I think I need to do it a bit differently though, but that’s more or less what I wanted. From the inner map I return an anonymous function with the running number as a parameter, flatten, zipWithIndex. And then finally apply the index on the anonymous function and execute it.
1

If you want it to be fast, just add another scope by adding braces. You can return values from that scope also, so there are really no downsides:

val documents = List("a" :: "b" :: Nil, "aaa" :: Nil)
val lettercount = {
  var currentPageNumber = 0
  documents.map(_.map(s => {
    currentPageNumber += 1
    println("Page " + currentPageNumber + " is " + s)
    s.length
  }).sum).sum
}
// currentPageNumber = 7  // Uncommented, this would be an error

2 Comments

You’re right, of course. But it’s not about speed. I just want to learn some more iterator tricks.
@Debilski - Then Ken's answer is the one you want. fold is the generic "carry information along" operation.
1

You can pull the initialization of currentPageNumber in the outer foreach:

val documents = List("a" :: "b" :: Nil, "aa" :: "bb" :: Nil, "aaa" :: Nil)

documents.foreach {
  var currentPageNumber = 0
  doc => for(page <- doc) {
    currentPageNumber += 1
    printf("%d: %s%n", currentPageNumber, page)
    // do something with page
  }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.