24

I am asking a very basic question which confused me recently. I want to write a Scala For expression to do something like the following:

for (i <- expr1) {
  if (i.method) {
    for (j <- i) {
      if (j.method) {
        doSomething()
      } else {
        doSomethingElseA()
      }
    }
  } else {
    doSomethingElseB()
  }
}

The problem is that, in the multiple generators For expression, I don't know where can I put each for expression body.

for {i <- expr1
  if(i.method) // where can I write the else logic ?
  j <- i 
  if (j.method)
} doSomething()

How can I rewrite the code in Scala Style?

AmigoNico
  • 6,652
  • 1
  • 35
  • 45
Sawyer
  • 15,581
  • 27
  • 88
  • 124
  • 1
    What’s wrong with the first example? – Debilski Nov 16 '10 at 16:27
  • Your question "title" is too pathetic for the problem that you defined ;) – Alexey Nov 18 '10 at 09:23
  • 1
    I came to this question because from the title I thought I would hear about problems introduced in the language by the existence of for-comprehensions. Cute titles are fun in a blog, but in order for readers to make productive use of StackOverflow as a problem-solving resource, it is important that titles convey what the question is about. A good title for this question would be "Need 'if..else' inside for-comprehension". – AmigoNico Nov 10 '12 at 22:11
  • Wow, surprised no-one has down-voted or voted to close based on the horrific title. Edit it. – Dave Griffith Nov 11 '12 at 01:33

6 Answers6

22

The first code you wrote is perfectly valid, so there's no need to rewrite it. Elsewhere you said you wanted to know how to do it Scala-style. There isn't really a "Scala-style", but I'll assume a more functional style and tack that.

for (i <- expr1) {
  if (i.method) {
    for (j <- i) {
      if (j.method) {
        doSomething()
      } else {
        doSomethingElseA()
      }
    }
  } else {
    doSomethingElseB()
  }
}

The first concern is that this returns no value. All it does is side effects, which are to be avoided as well. So the first change would be like this:

val result = for (i <- expr1) yield {
  if (i.method) {
    for (j <- i) yield {
      if (j.method) {
        returnSomething()
        // etc

Now, there's a big difference between

for (i <- expr1; j <- i) yield ...

and

for (i <- expr1) yield for (j <- i) yield ...

They return different things, and there are times you want the later, not the former. I'll assume you want the former, though. Now, before we proceed, let's fix the code. It is ugly, difficult to follow and uninformative. Let's refactor it by extracting methods.

def resultOrB(j) = if (j.method) returnSomething else returnSomethingElseB
def nonCResults(i) = for (j <- i) yield resultOrB(j)
def resultOrC(i) = if (i.method) nonCResults(i) else returnSomethingC
val result = for (i <- expr1) yield resultOrC(i)

It is already much cleaner, but it isn't returning quite what we expect. Let's look at the difference:

trait Element
object Unrecognized extends Element
case class Letter(c: Char) extends Element
case class Punct(c: Char) extends Element
val expr1 = "This is a silly example." split "\\b"

def wordOrPunct(j: Char) = if (j.isLetter) Letter(j.toLower) else Punct(j)
def validElements(i: String) = for (j <- i) yield wordOrPunct(j)
def classifyElements(i: String) = if (i.nonEmpty) validElements(i) else Unrecognized
val result = for (i <- expr1) yield classifyElements(i)

The type of result there is Array[AnyRef], while using multiple generators would yield Array[Element]. The easy part of the fix is this:

val result = for {
  i <- expr1
  element <- classifyElements(i)
} yield element

But that alone won't work, because classifyElements itself returns AnyRef, and we want it returning a collection. Now, validElements return a collection, so that is not a problem. We only need to fix the else part. Since validElements is returning an IndexedSeq, let's return that on the else part as well. The final result is:

trait Element
object Unrecognized extends Element
case class Letter(c: Char) extends Element
case class Punct(c: Char) extends Element
val expr1 = "This is a silly example." split "\\b"

def wordOrPunct(j: Char) = if (j.isLetter) Letter(j.toLower) else Punct(j)
def validElements(i: String) = for (j <- i) yield wordOrPunct(j)
def classifyElements(i: String) = if (i.nonEmpty) validElements(i) else IndexedSeq(Unrecognized)
val result = for {
  i <- expr1
  element <- classifyElements(i)
} yield element

That does exactly the same combination of loops and conditions as you presented, but it is much more readable and easy to change.

About Yield

I think it is important to note one thing about the problem presented. Let's simplify it:

for (i <- expr1) {
  for (j <- i) {
    doSomething
  }
}

Now, that is implemented with foreach (see here, or other similar questions and answer). That means the code above does exactly the same thing as this code:

for {
  i <- expr1
  j <- i
} doSomething

Exactly the same thing. That is not true at all when one is using yield. The following expressions do not yield the same result:

for (i <- expr1) yield for (j <- i) yield j

for (i <- expr1; j <- i) yield j

The first snippet will be implemented through two map calls, while the second snippet will use one flatMap and one map.

So, it is only in the context of yield that it even makes any sense to worry about nesting for loops or using multiple generators. And, in fact, generators stands for the fact that something is being generated, which is only true of true for-comprehensions (the ones yielding something).

Community
  • 1
  • 1
Daniel C. Sobral
  • 295,120
  • 86
  • 501
  • 681
  • your answer is helpful, can I say that, if I don't use 'for' to return a value, I'd better not use the generator syntax and use other syntax instead? – Sawyer Nov 17 '10 at 06:59
  • @ZZcat It's not that it is "better" to use one syntax or another when not using `yield`. It's just that it doesn't matter either way, so you may choose whatever feels best for you. – Daniel C. Sobral Nov 17 '10 at 10:40
4

The part

for (j <- i) {
   if (j.method) {
     doSomething(j)
   } else {
     doSomethingElse(j)
   }
 }

can be rewritten as

for(j <- i; e = Either.cond(j.method, j, j)) {
  e.fold(doSomething _, doSomethingElse _)  
}  

(of course you can use a yield instead if your do.. methods return something)

Here it is not so terrible useful, but if you have a deeper nested structure, it could...

Landei
  • 54,104
  • 13
  • 100
  • 195
  • I always doubt that, whether shorter code runs really faster than longer code, somethings nested high order functions have a O(n square) complexity, but they looks really short. And people are heavily use these high order functions, I think this is potentially harmful. I am not sure that I am correct, any correction of my point is welcome. – Sawyer Nov 16 '10 at 13:22
  • @ZZCat How critical is performance to your use-case, how many thousand entries are you mapping over? – Kevin Wright Nov 16 '10 at 13:23
  • @Kevin, off topic though, it's not about how many thousand entries, it's about how many thousand inefficient For loops you write in your entire Scala programming life. Well, "Faster" is a very disputatious word, I would use it with caution. – Sawyer Nov 16 '10 at 14:05
  • @ZZCat it's also about premature optimisation being the root of all evil. You can often get far better performance gains by being able to see your algorithm clearly and concisely - then seeing a better overall design - instead of early micro-optimising at the loop level – Kevin Wright Nov 16 '10 at 14:56
  • 1
    @ZZcat Well, I assume this complex code is being used with one line: the method call. If it is being copy&pasted everywhere, then you have other problems. If it is being used with a method call, and the programmers using it assess its complexity by looking at the implementation instead of the documentation, then you have other problems too. – Daniel C. Sobral Nov 16 '10 at 16:34
3
import scalaz._; import Scalaz._

val lhs = (_ : List[X]) collect { case j if j.methodJ => doSomething(j) } 
val rhs = (_ : List[X]) map doSomethingElse
lhs <-: (expr1 partition methodI) :-> rhs
oxbow_lakes
  • 133,303
  • 56
  • 317
  • 449
2

You can not. The for(expr; if) construct just filter the element that must be handled in the loop.

Nicolas
  • 24,509
  • 5
  • 60
  • 66
  • I know filter expression can't have an else statement, I want to know how that part of logic be expressed in Scala style. – Sawyer Nov 16 '10 at 12:24
  • Yes, It just means that the monad-for that you want to use here is not the easiest solution in YOUR case. It does not mean that it is a syntax poison, it's just not the adequate tool. – Nicolas Nov 16 '10 at 13:52
  • Yeah, it's not that bad, I just wanna hear more people's opinions about it. My example is not a particular case, I think it's quite common in any programming language. – Sawyer Nov 16 '10 at 14:12
  • This answer relates only to the second code example. You can't have else statement in the for-expression generator but you can use if/else within the body, as in the first code – pagoda_5b Nov 13 '12 at 10:38
1

If the order isn't important for the calls to doSomething() and doSomethingElse() then you can rearrange the code like this.

val (tmp, no1) = expr1.partition(_.method)
val (yes, no2) = tmp.partition(_.method)

yes.foreach(doSomething())
no1.foreach(doSomethingElse())
no2.foreach(doSomethingElse())

To answer your original question, I think that for comprehensions can be quite nice for specific use cases, and your example doesn't fit nicely.

Craig P. Motlin
  • 26,452
  • 17
  • 99
  • 126
0

The conditions specified in a Scala for operation act to filter the elements from the generators. Elements not satisfying the conditions are discarded and are not presented to the yield / code block.

What this means is that if you want to perform alternate operations based on a conditional expression, the test needs to be deferred to the yield / code block.

Also be aware that the for operation is relatively expensive to compute (currently) so perhaps a simpler iterative approach might be more appropriate, perhaps something like:

expr1 foreach {i =>
  if (i.method) {
    i foreach {j =>
      if (j.method)
        doSomething()
      else
        doSomethingElseA()
    }
  }
  else
    doSomethingElseB()
}

Update:

If you must use a for comprehension and you can live with some restrictions, this might work:

for (i <- expr1; j <- i) {
  if (i.method) {if (j.method) doSomething() else doSomethingElseA()} else doSomethingElseB()
}
Don Mackenzie
  • 7,953
  • 7
  • 31
  • 32
  • 1
    This absolutely doesn't look better than the imperative style. And it seems that all the "syntax sugar" of the For expression is meaningless as long as it has a poor performance. – Sawyer Nov 16 '10 at 12:22
  • @ZZcat, I think the for construct (comprehension) is very useful, it is capable of multi level iteration, filtering and intermediate assignment leading to optional return of a result collection, unfortunately, I think it is not a very natural fit with the code you presented above. – Don Mackenzie Nov 16 '10 at 13:58
  • Yeah, I agree with "multi level iteration" and "filtering", just cannot find a way easily do some "Side Effects". In File and IO programming, I always want to do some operations on files, not care about returning a value. I can hardly find a place to place my code to complete the operations. – Sawyer Nov 16 '10 at 14:17
  • Thanks, I'm sorry for the misleading, the two doSomethingElse() doesn't have to be the same, I've updated my code, really sorry. – Sawyer Nov 16 '10 at 15:22