How to cut a for-comprehension short (break out of it) in scala?
Asked Answered
U

5

9

I have a piece of code which would code as follows:

val e2 = for (e <- elements if condition(expensiveFunction(e))) yield {
            expensiveFunction(e)
         }

Where the condition will be true for a few elements and then become false for all remaining ones.

Unfortunately this doesn't work (even if I ignore performance) because my elements is an infinite iterator.

Is there a way to use a "break" in a for-comprehension so it stops yielding elements when a certain condition holds? Otherwise, what would be the scala-idiomatic way to compute my e2?

Unwish answered 28/11, 2012 at 1:38 Comment(8)
possible duplicate of How do I break out of a loop in Scala?Wheelsman
That question is about a regular loop, not a for-comprehension (i.e., with yield). I suspect takeWhile can be part of the solution...Unwish
The question is somehow different, but answers (as far as I understand) can be applied to your situation as wellWheelsman
for-comprehensions are completely different than traditional loops under the covers. The fact that they share similar syntax is to make them easier to read.Lackluster
The duplicate is #13344031Shaikh
That's not a duplicate either, as it asks for only one item, not a few like this one.Unwish
Possible duplicate of How do I break out of a loop in Scala?Cislunar
Yes, the solution is the same, but that question is ill-phrased: a "for (<-)" is a comprehension, not a loop.Unwish
S
9
scala> def compute(i: Int) = { println(s"f$i"); 10*i }
compute: (i: Int)Int

scala> for (x <- Stream range (0, 20)) yield compute(x)
f0
res0: scala.collection.immutable.Stream[Int] = Stream(0, ?)

scala> res0 takeWhile (_ < 100)
res1: scala.collection.immutable.Stream[Int] = Stream(0, ?)

scala> res1.toList
f1
f2
f3
f4
f5
f6
f7
f8
f9
f10
res2: List[Int] = List(0, 10, 20, 30, 40, 50, 60, 70, 80, 90)

Edit, another demonstration:

scala> def compute(i: Int) = { println(s"f$i"); 10*i }
compute: (i: Int)Int

scala> for (x <- Stream range (0, 20)) yield compute(x)
f0
res0: scala.collection.immutable.Stream[Int] = Stream(0, ?)

scala> res0 takeWhile (_ < 100)
res1: scala.collection.immutable.Stream[Int] = Stream(0, ?)

scala> res1.toList
f1
f2
f3
f4
f5
f6
f7
f8
f9
f10
res2: List[Int] = List(0, 10, 20, 30, 40, 50, 60, 70, 80, 90)

scala> Stream.range(0,20).map(compute).toList
f0
f1
f2
f3
f4
f5
f6
f7
f8
f9
f10
f11
f12
f13
f14
f15
f16
f17
f18
f19
res3: List[Int] = List(0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190)

scala> Stream.range(0,20).map(compute).takeWhile(_ < 100).toList
f0
f1
f2
f3
f4
f5
f6
f7
f8
f9
f10
res4: List[Int] = List(0, 10, 20, 30, 40, 50, 60, 70, 80, 90)
Shaikh answered 28/11, 2012 at 5:44 Comment(9)
In my terminology, that is (for(e <- elements) yield expensiveFunction(e)).takeWhile(r => condition(r)). I am surprised at how obvious it is. Looks like reasoning about infinite iterators is still difficult for me.Unwish
@Unwish note (for(...)...) takeWhile condition. Placeholder syntax, leaving off the placeholder because of expected type. And losing the dot and parens. Others have got me hooked on minimalism.Shaikh
This seems to compute the expensiveFunction for all elements - and truncate the resulting sequence only after the computation.Ettieettinger
@javadba What do you observe that suggests that? Compare Stream.range(0,20).map(compute).toList and Stream.range(0,20).map(compute).takeWhile(_ < 100).toList. The collection is not realized after each operation from left to right.Shaikh
@javadba it depends on the collection used. For strict collections it will compute all elements, for lazy ones it will not.Yeomanry
I tested this on a non-lazy collection - by adding print statements before each return value: the expensiveFucnction gets invoked on all elements and then the result is truncated. That is not the case for the toIterator approach from @om-nom-nom: that one correctly invokes expensiveFunction` only the min required times.Ettieettinger
@javadba Stream is lazy, is the point. I hope that clears it up. As you can see, your previous assertion about my example is not correct.Shaikh
@javadba sorry, I just looked at the question again, the point was how to take a prefix, so actually strict was beside the point. In the comments, there are a couple of near-duplicates that didn't satisfy the OP.Shaikh
OK it's more clear now : the answer shown here is intended for lazy collections. It's not clear to me what the OP intended: but in any case useful to have the distinction of answers supporting both approaches to collections . I will leave my comment above since i can not edit it - and deleting it would lose the context of the thread.Ettieettinger
W
20

You could go with lazy approach:

val e2 = elements.toIterator                    
          .map(expensiveFunction)
          .takeWhile(result => result == true) // or just .takeWhile(identity)
// you may want to strict iterator, (e.g. by calling .toList) at the end

So you compute expensiveFunction on demand, and if there is false on some step, you won't do unnecessary work.

Wheelsman answered 28/11, 2012 at 2:2 Comment(5)
+1 -- still, because my particular case is more clearly written as a for-comprehension, I'm accepting som-snytt's answer. Thanks.Unwish
This may actually be the best answer: the one from @Shaikh invokes the expensiveFunction for all elements - and only afterwards discards the unwanted results.Ettieettinger
@javadba No, for comprehension desugars to map. I think my answer was to the question about for? Otherwise they are the same.Shaikh
I used may in a polite manner but actually I had tested this and verified that your approach is not greedy whereas that from @Shaikh does incur full cost of the expensiveFunction even on unused/unneeded elements.Ettieettinger
The recent comments about lazy/strict are interesting because elements is already an iterator, per the question.Shaikh
S
9
scala> def compute(i: Int) = { println(s"f$i"); 10*i }
compute: (i: Int)Int

scala> for (x <- Stream range (0, 20)) yield compute(x)
f0
res0: scala.collection.immutable.Stream[Int] = Stream(0, ?)

scala> res0 takeWhile (_ < 100)
res1: scala.collection.immutable.Stream[Int] = Stream(0, ?)

scala> res1.toList
f1
f2
f3
f4
f5
f6
f7
f8
f9
f10
res2: List[Int] = List(0, 10, 20, 30, 40, 50, 60, 70, 80, 90)

Edit, another demonstration:

scala> def compute(i: Int) = { println(s"f$i"); 10*i }
compute: (i: Int)Int

scala> for (x <- Stream range (0, 20)) yield compute(x)
f0
res0: scala.collection.immutable.Stream[Int] = Stream(0, ?)

scala> res0 takeWhile (_ < 100)
res1: scala.collection.immutable.Stream[Int] = Stream(0, ?)

scala> res1.toList
f1
f2
f3
f4
f5
f6
f7
f8
f9
f10
res2: List[Int] = List(0, 10, 20, 30, 40, 50, 60, 70, 80, 90)

scala> Stream.range(0,20).map(compute).toList
f0
f1
f2
f3
f4
f5
f6
f7
f8
f9
f10
f11
f12
f13
f14
f15
f16
f17
f18
f19
res3: List[Int] = List(0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190)

scala> Stream.range(0,20).map(compute).takeWhile(_ < 100).toList
f0
f1
f2
f3
f4
f5
f6
f7
f8
f9
f10
res4: List[Int] = List(0, 10, 20, 30, 40, 50, 60, 70, 80, 90)
Shaikh answered 28/11, 2012 at 5:44 Comment(9)
In my terminology, that is (for(e <- elements) yield expensiveFunction(e)).takeWhile(r => condition(r)). I am surprised at how obvious it is. Looks like reasoning about infinite iterators is still difficult for me.Unwish
@Unwish note (for(...)...) takeWhile condition. Placeholder syntax, leaving off the placeholder because of expected type. And losing the dot and parens. Others have got me hooked on minimalism.Shaikh
This seems to compute the expensiveFunction for all elements - and truncate the resulting sequence only after the computation.Ettieettinger
@javadba What do you observe that suggests that? Compare Stream.range(0,20).map(compute).toList and Stream.range(0,20).map(compute).takeWhile(_ < 100).toList. The collection is not realized after each operation from left to right.Shaikh
@javadba it depends on the collection used. For strict collections it will compute all elements, for lazy ones it will not.Yeomanry
I tested this on a non-lazy collection - by adding print statements before each return value: the expensiveFucnction gets invoked on all elements and then the result is truncated. That is not the case for the toIterator approach from @om-nom-nom: that one correctly invokes expensiveFunction` only the min required times.Ettieettinger
@javadba Stream is lazy, is the point. I hope that clears it up. As you can see, your previous assertion about my example is not correct.Shaikh
@javadba sorry, I just looked at the question again, the point was how to take a prefix, so actually strict was beside the point. In the comments, there are a couple of near-duplicates that didn't satisfy the OP.Shaikh
OK it's more clear now : the answer shown here is intended for lazy collections. It's not clear to me what the OP intended: but in any case useful to have the distinction of answers supporting both approaches to collections . I will leave my comment above since i can not edit it - and deleting it would lose the context of the thread.Ettieettinger
B
3

You can just use takeWhile:

elements.takeWhile(condition(expensiveFunction(_)))
Buseck answered 28/11, 2012 at 1:47 Comment(1)
I could use elements.takeWhile(e => condition(expensiveFunction(e)), but then I would need to re-compute the expensiveFunction for each of them again.Unwish
U
0

Found this solution:

(for (e <- elements) yield {
  val x= expensiveFunction(e)
  if (condition(x)) Some(x) else None
}).takeWhile(_.nonEmpty).map(_.get)

A better one, anyone?

Unwish answered 28/11, 2012 at 1:58 Comment(0)
B
0

Here is my idea: If the elements is a lazy container already(like Stream, Iterator):

(for (e <- elements; 
      buf = expensiveFunction(e);  
      if condition(buf)) yield buf).headOption

Or not:

(for (e <- elements.view; 
      buf = expensiveFunction(e);  
      if condition(buf)) yield buf).headOption
Boxhaul answered 28/11, 2012 at 2:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.