0

I'm trying to understand how for comprehension works by understanding the difference of the two below:

case class Item(name: String, value: Int)

def forCompWithVarAssignment() = {
      val itemList = List(Item("apple", 10), Item("orange", -5), Item("coke", -2), Item("ensaymada", 20))
      var someVar = 1

      for {
        item <- itemList
        if someVar > 0
        itemValue = item.value
        if itemValue < 0
      } {
        println(s"got itemValue: $itemValue")
        println(s"setting someVar to negative value.")
        someVar = -1
      }
}

def forCompWithOption() = {
  val itemList = List(Item("apple", 10), Item("orange", -5), Item("coke", -2), Item("ensaymada", 20))
  var someVar = 1

 for {
    item <- itemList
    if someVar > 0
    itemValue <- Option(item.value)
    if itemValue < 0
  } {
    println(s"got itemValue: $itemValue")
    println(s"setting someVar to negative value.")
    someVar = -1
  }
}

Now, the only difference of forCompWithVarAssignment() and forCompWithOption() are itemValue = item.value and itemValue <- Option(item.value) respectively.

Which, I believe, itemValue gets the same thing either way.

However, here's the tricky part.

The result of the forCompWithVarAssignment() is as follows:

got itemValue: -5
setting someVar to negative value.
got itemValue: -2
setting someVar to negative value.

While the result of forCompWithOption() is:

got itemValue: -5
setting someVar to negative value.

Question:

On forCompWithVarAssignment(), why did the loop still continued even if someVar is already -1 after it passes Item("orange", -5)?

I'm expecting that the loop will stop at Item("orange", -5) so got itemValue: -2 should have never been printed.

1 Answer 1

2

So, your first function is equivalent to something like this (I am simplifying a bit for readability):

itemList
  .withFilter { _ => someVar > 0 }
  .map { case Item(_, value) => value }
  .withFilter(_ < 0)
  .foreach { itemValue => 
     println(s"got itemValue: $itemValue")
     println(s"setting someVar to negative value.")
     someVar = -1
   }

What happens here is that the list is filtered first, then values are extracted, then second filter is applied, and finally the "loop body" is executed for every element. When the first filter is run, someVar value 1, and every item matches. When you set it to -1 at the end eventually, it is already too late, and does not matter for anything.

The second function is different, it is equivalent to something like this:

itemList
  .withFilter { _ => someVar > 0 }
  .foreach { case Item(_, value) => 
    Option(value).withFilter(_ < 0).foreach { itemValue => 
      println(s"got itemValue: $itemValue")
      println(s"setting someVar to negative value.")
      someVar = -1
    }how 
  } 

Here the "body" is nested inside the .foreach which is applied to result of the first filter. .withFilter is lazy, it checks each element, and then, if it matches, passes it to foreach before moving on to the next one. So, when foreach body hits the first negative value, it sets someVar to -1 and is never called again.

This is the difference between your two functions. If you ask me why two pieces of code that look so similar end up being compiled so differently, my answer is going to be "this is just how it works".

If you want to know how you can avoid stepping into weirdness like this in the future, the best advice I can give you is going to be: DO NOT USE VARS, pretty much, ever. There are, perhaps, 0.1% of use cases you'll encounter when you actually need a var in scala, but your best bet is to just pretend var does not exist at all until you gather enough command of the language to be able to definitively distinguish those 0.1% of cases from others, and to know for sure how to use it correctly.

You might also find it helpful to get into a habit of avoiding using for-comprehensions as a stand-in for "loop". In many cases you don't really need it, and the code that spells out the transformations explicitly (kinda like I did above) is not only more clear and readable, but also helps to avoid confusion about what is actually happening like the one you had here.

For example, what you are doing here can easily be accomplished without vars, fors, and horrible side effects with something like this:

itemList
   .collectFirst { case Item(_, value) if value < 0 => value }
   .foreach { case itemValue => println (s"got itemValue: $itemValue") }
Sign up to request clarification or add additional context in comments.

2 Comments

One thing I still could not grasp: why does the second function have no .map()? Is it safe to assume that this operation: <- is equivalent to .forEach()? @Dima
@CodeGeek like I said, it's just how it works. Sometimes it's map, sometimes a flatMap, sometimes foreach ... It is complicated and is not really "safe to assume" anything. That is why it is so important to avoid mutations and side effects happening inside of it. If you didn't have those, you would notice no difference and would never need to known or care about these subtle differences in the implementation.

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.