Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement breakable generators for sequence comprehensions #9120

Closed
scabug opened this issue Jan 28, 2015 · 12 comments
Closed

Implement breakable generators for sequence comprehensions #9120

scabug opened this issue Jan 28, 2015 · 12 comments

Comments

@scabug
Copy link

scabug commented Jan 28, 2015

Proposing a light-weight implementation of 'break' and 'continue' that operate inside of sequence comprehensions, as specialized breakable generators.

The concept is written up here:
http://erikerlandson.github.io/blog/2015/01/24/monadic-break-and-continue-for-scala-sequence-comprehensions/

A simple example of what they look like in use:

object Main {
  import scala.util.control.BreakableGenerators._

  def main(args: Array[String]) {

    val r = for (
      // generate a breakable sequence from some sequential input
      loop <- breakable(1 to 1000);
      // iterate over the breakable sequence
      j <- loop;
      // print out at each iteration
      _ = { println(s"iteration j= $j") };
      // continue to next iteration when 'j' is even
      if { j % 2 == 0 } continue;
      // break out of the loop when 'j' exceeds 5
      if { j > 5 } break(loop) 
    ) yield {
      j
    }
    println(s"result= ${r.toList}")
  }
}
@scabug
Copy link
Author

scabug commented Jan 28, 2015

Imported From: https://issues.scala-lang.org/browse/SI-9120?orig=1
Reporter: @erikerlandson
Assignee: @erikerlandson

@scabug
Copy link
Author

scabug commented Jan 29, 2015

@erikerlandson said:
pull req: scala/scala#4275

@scabug
Copy link
Author

scabug commented Jan 29, 2015

@Ichoran said:
Can you provide a bit more motivation for why it is important to have this syntax?

For instance, your for-loop could be more easily expressed by

val r = (1 to 1000).iterator.
  map{ j => println(s"iteration j = $j") }.
  filterNot(j => (j % 2) == 0).
  takeWhile(_ <= 5)

If you are making instead an argument about efficiency, it would help to show benchmarks. It is far from clear that your approach yields a nice tradeoff between speed and power (i.e. as much of both as possible).

@scabug
Copy link
Author

scabug commented Jan 29, 2015

@erikerlandson said:
The particular examples I've been using were chosen for ease of understanding, as opposed to motivation. The motivating cases are loops having multiple exit points, and/or continuation points. (continue itself is just the inversion of a regular if guard, I just included it for completeness).

Another way of looking at it: motivation is more or less the same as the already-existing scala.util.control.Breaks, but: (a) not requiring throw/catch under the hood, and (b) all looping constructs remain nicely scoped inside the for construct.

The times I, personally, find myself wanting break and/or continue, are usually numeric or other complex looping algorithms where there are often multiple conditions that might indicate loop halting, and those conditions are best tested at varying points in the code, which makes break/continue tests a nice representation; they can be dropped in wherever they are warranted, without breaking up the main flow of the code. Other use cases include loops involving many varieties of error checking that become relevant at multiple points in the progress of the main looping body.

@scabug
Copy link
Author

scabug commented Jan 30, 2015

@Ichoran said:
Since continue is just an inversion of if, is it actually important to have?

Also, can you give an example which is not more compactly expressed with takeWhile and a regular iterator?

I think it is valuable to have multiple ways to do things as long as some of them are clearly superior in some cases. But otherwise it seems to be a net negative (Perl vs. Python for instance) to have many different approximately equivalent ways to do the same thing: it makes reading code harder since the code-to-intent map is more complex.

FWIW, the normal breakable/break provides a way to exit closures and such that has less boilerplate and is faster than the stackless exception-based return that the compiler will give you. A similarly compelling story here (whether for speed or clarity or both or something else) would be nice to have. Also, any advantage has to be weighed against the potential for accidental consumption of the loop iterator.

@scabug
Copy link
Author

scabug commented Jan 30, 2015

@Ichoran said:
Played around with it a little more--correct me if I'm wrong, but isn't the behavior of break(outer) kind of nonintuitive in nested loops? I would naively expect it to immediately break out of the outer context, but it looks to me like it means something like "finish the inner loop, but don't run any more iterations on the outer loop after that".

Except it's a little more fiddly because anything where break(outer) is triggered will be filtered out of the inner loop. So if you consistently break on the rest of the inner loop, you actually act as if you broke out of both loops. But otherwise it acts like a filter only on the last iteration.

@scabug
Copy link
Author

scabug commented Jan 30, 2015

@erikerlandson said:
Correct, if you say break(outer) instead of break(inner, outer), it doesn't have any scoping knowledge to say "oh, I should probably also break out of inner as well." It would be pretty easy to support something like inner <- breakable(data)(outer), so that outer could know to halt inner if it was broken. But that isn't enforceable either, of course. I could maintain a stack internal to BreakableGenerators. I'd have to think through whether that causes any concurrency-safety issues. Past that, it's getting into compiler support, I think.

@scabug
Copy link
Author

scabug commented Feb 2, 2015

@Ichoran said:
Others may feel differently, but I am not yet convinced that this addition to the library is desirable.

Upsides:

  • convenient early termination in for comprehensions
  • complements breakable/break in that it works with yield

Downsides:

  • fragile syntax (perhaps at least partially fixable)
  • more ways to do the same thing (learning / maintenance cost goes up)
  • more boilerplate than existing ways to do it

It may be just the thing for some users, which argues for it being part of some library, but not necessarily the standard library.

@scabug
Copy link
Author

scabug commented Feb 2, 2015

@erikerlandson said:
Rex, thanks for trying it out! I have a few ideas about how to make it more robust. If that pans out, I'll update the PR.

@scabug
Copy link
Author

scabug commented Mar 9, 2017

@erikerlandson said:
I came up with a different design that meets all my goals for API and semantics. I published it as a 3rd-party package, but it might be interesting to the scala-lang community:

https://github.com/erikerlandson/breakable

@SethTisue
Copy link
Member

SethTisue commented Feb 19, 2024

I think this is now out of scope for Scala 2, but Scala 3's boundary and break might be what you were hoping for @erikerlandson? If you see further improvements that should be made, https://contributors.scala-lang.org is now the place for these sorts of discussions.

@SethTisue SethTisue closed this as not planned Won't fix, can't repro, duplicate, stale Feb 19, 2024
@Ichoran
Copy link

Ichoran commented Feb 19, 2024

@SethTisue - I don't think boundary/break entirely works because you can't get partial computations that way (not with ordinary syntax). The features he wanted could be built with boundary/break, but they alone aren't enough.

@erikerlandson - My kse3 library implements functionality like this for arrays using boundary/break. It doesn't solve the break-out-of-multiple-levels problem (by design), but operations that normally accumulate things can return partial results with skipping and early termination. If you wanted to do it for the Scala collections, it might be instructive to see how I did it for arrays.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants