Reputation: 925
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.
Upvotes: 0
Views: 189
Reputation: 40500
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 value
s 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, for
s, 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") }
Upvotes: 2