[racket-dev] Short-circuiting comprehensions

From: Matthew Flatt (mflatt at cs.utah.edu)
Date: Fri Sep 14 16:25:45 EDT 2012

At Fri, 14 Sep 2012 15:30:22 -0400, Eli Barzilay wrote:
> Four hours ago, Matthew Flatt wrote:
> > 
> > Also, I think the names `#:while' and `#:until' are too close to
> > `#:when' and `#:unless'. I suggest `#:break-when' and `#:break-unless'.
> > Compare:
> > 
> >  > (for*/list ([j 2] [i 10] #:when (i . < . 5)) i)
> >  '(0 1 2 3 4 0 1 2 3 4)
> >  > (for*/list ([j 2] [i 10] #:break-unless (i . < . 5)) i)
> >  '(0 1 2 3 4)
> > 
> > I imagine that `#:break-when' and `#:break-unless' are allowed among
> > the clauses much like `#:when' and `#:unless', but also allowed at the
> > end of the body. Is that what you had in mind?
> 
> Sorry for the bike-shedding, but to me that `#:break-unless' is even
> harder to read than `#:until'.  Possible explanation: "break unless"
> makes me parse two words and figure out how they combine, and "while"
> is something that I know without doing so.

After trying out various options, I agree that `break-when' is too many
words. I'm currently trying just `break' and dropping `break-unless',
and that feels better.

At Fri, 14 Sep 2012 11:49:20 -0400, Carl Eastlund wrote:
> I agree that #:while and #:until are easily confused with #:when and
> #:unless.  I slightly prefer #:stop- to #:break- as a prefix here, it seems
> a more natural word.

I like "break" because it goes with "for", it avoids potential
confusion with "stop" in `stop-after' and `stop-before', and it is more
clearly different from "final" (which is used below).

> I like the idea of allowing these clauses at the end
> of the body to give a notion of stopping after the current iteration.

It turns out that allowing `#:break' at the very end of the body causes
problems with scope and composition.

Imagine trying to implement `for/set' by wrapping its body in an
expansion to `for/fold'. Maybe you can look from the end of the body
and skip back over `#:break' clauses, but now imagine that the last
form before `break' expands to a combination of a definition and an
expression, and the `#:break' clause wants to refer to the identifier
that is bound in the expansion. We could probably make all that work
with `local-expand' and internal-definition contexts, but that gets
complicated.

Requiring an expression at the end of a `for' body --- while allowing
`#:break' clauses otherwise interspersed in the body --- avoids
composition and scope problems. Macros still have to do a little work
to juggle `#:break' clauses, but a `split-for-body' helper function is
straightforward to use.

Meanwhile, to support breaking after the current element, I'm trying
out `#:final'. A `#:final' clause is like `#:break', except that it
ends the loop after the next run of the body. (The two kinds of clauses
closely related to `stop-before' and `stop-after'.)

> (for/list ([i 10]) #:break (= i 2) i)
'(0 1)
> (for/list ([i 10]) #:final (= i 2) i)
'(0 1 2)

> (for ([book '("Guide" "Story" "Reference")]
        #:break (equal? book "Story")
        [chapter '("Intro" "Details" "Conclusion")])
    (printf "~a ~a\n" book chapter))
Guide Intro
Guide Details
Guide Conclusion

> (for* ([book '("Guide" "Story" "Reference")]
         [chapter '("Intro" "Details" "Conclusion")])
    #:break (and (equal? book "Story")
                 (equal? chapter "Conclusion"))
    (printf "~a ~a\n" book chapter))
Guide Intro
Guide Details
Guide Conclusion
Story Intro
Story Details

> (for* ([book '("Guide" "Story" "Reference")]
         [chapter '("Intro" "Details" "Conclusion")])
    #:final (and (equal? book "Story")
                 (equal? chapter "Conclusion"))
    (printf "~a ~a\n" book chapter))
Guide Intro
Guide Details
Guide Conclusion
Story Intro
Story Details
Story Conclusion

> (for ([book '("Guide" "Story" "Reference")]
        #:final (equal? book "Story")
        [chapter '("Intro" "Details" "Conclusion")])
    (printf "~a ~a\n" book chapter))
Guide Intro
Guide Details
Guide Conclusion
Story Intro


Posted on the dev mailing list.