The Common Lisp LOOP Macro for Racket
(require (for-syntax racket) (for-syntax "set-values.rkt") "set-values.rkt" racket/generator) <define-end-of-generator> <local-macros> <supporting-functions> <return> <add-cond-clause> <make-hash-generator> <all-the-rest> (define (macroexpand-1 datum) (syntax->datum (expand-once datum))) (provide macroexpand-1)
1 Introduction
This is an implementation of Common Lisp’s LOOP macro for Racket. The LOOP macro is similar to all of Racket’s for/* macros, combined with Python’s for loop, except it’s more powerful than either.
Examples:
(define (sift pred? list) (loop for value in list when (pred? value) consing value into gold else consing value into dirt finally (return (values (reverse gold) (reverse dirt)))))
> (loop for x in '(a b c d e f g) for y from 0 when (even? y) collect x)
→ (a c e g)
> (loop for x in '(a b c d e f g) for y in '(1 2 3 4 5 6 7) with-collection-type hash collect (cons x y))
→ #hash((g . 7) (b . 2) (a . 1) (c . 3) (d . 4) (e . 5) (f . 6))
LOOP can also do the job of for/and:
(loop for x in a-list for y in another-list always (and (number? x) (symbol? y)))
...or for/or:
(loop for x in a-list thereis (symbol? x))
...or for/sum:
(loop for x in a-list when (integer? (sqrt x)) sum x)
...or you can convert a list into a hash table:
(loop with collection-type 'hash/immutable for key-value in '((key . val) (key2 . val2)) collect key-value)
...or you can write an old-fashioned while loop:
(loop for line = (read-line socket) while (not (eof-object? line)) do (display line) (newline) finally (close-input-port socket))
The loop macro can also iterate over generators as defined in the racket/generator package.
(loop for item in (gen) do (displayln item))
Since racket/generator provides no non-ambiguous way to end a generator, arrange for your generator to yield the value end-of-generator to terminate the loop, or use an explicit return clause to exit.
(define-struct end-of-generator* ()) (define end-of-generator (make-end-of-generator*)) (define end-of-generator? end-of-generator*?) (provide end-of-generator end-of-generator?)
2 Enabling return
In Common Lisp, the LOOP macro is often used in conjunction with return and return-from. This library defines return as a macro which invokes a continuation that can be tucked away within this module.
return is defined as a macro and not a function because in Common Lisp, it’s legal to do this:
(return (values 1 2 3 4))
...where a function would not be able to receive the multiple values. return and its hidden continuation are defined as follows:
(define return-cc (make-parameter #f)) (define-syntax return (syntax-rules () ((_) (return (void))) ((_ value-form) (call-with-values (λ () value-form) (λ all-values (apply (return-cc) all-values)))))) (define-syntax return-from (syntax-rules () ((_ block-name value-form) (parameterize ((return-cc block-name)) (return value-form))))) (provide return return-from)
When the LOOP macro is invoked, it sets the return-cc parameter with its own continuation, which is only an escape continuation.
3 The Main Body
3.1 Variables used during macro expansion
The traditional Scheme way to write anything at all is to define all the variables as arguments to a recursive loop-function, and to change those variables, you pass every variable to the next iteration of the loop-function, giving new values for the variables that should be different for the next iteration. At first I began with a design like that (and most of the supporting functions are still written this way), but as the number of variables grew, it became more than a little difficult to pass all of them as arguments at every point in the program where the loop-function was called. Every time a new variable was added, it was necessary to go back and change all the points where recursion took place. A big chunk of the code I wrote this way had to be deleted and rewritten from scratch.
As a result, I ended up taking most of the variables out of the loop-function’s argument list and just changing them with set!. It may be less "Rackety", but it gets the job done.
The main body of the macro iterates over all the clauses and builds the following variables:
(define call-with-cc #'call/ec) (define return-continuation #'loop-return) (define collection #'collection) (define initial-collection #'#f) (define count* #'count) (define initial-count #'#f) (define sum* #'sum) (define min #'min) (define max #'max) (define reverse? #'reverse?) (define reverse-value #'#t) (define string-accumulator #'string-accumulator) (define initial-string #'#f) (define initial-sum #'#f) (define and? #f) (define conditional-stack '()) (define collection-type #''list) (define prologue #'()) (define initially-prologue #'()) (define epilogue #'()) (define iterations #'()) (define let-values-defs #'()) (define current-condition #'()) (define loop-conditions #'()) (define preiterations #'()) (define loop-preconditions #'()) (define action-clauses #'()) (define current-cond-body #'()) (define body #'()) (define list-defs #'()) (define let-defs #'()) (define gnarled-let-defs #'()) (define current-gnarled-let-def #'())
finally is the finally clause, which gets executed after everything else. It can be bypassed with a return form. Often, a return form is used in a finally clause to cause the loop to return loop-local variables.
iterations is a syntax-list of all the variables that must be changed with each iteration of the loop, along with a snippet of code that does the change. An example of what might be in this variable is #'((variable-name (add1 variable-name)) (var2 (cdr var2))) The iterations get executed just as the loop is recursing into the next iteration
current-condition is a list of boolean clauses that is built while processing if clauses.
loop-conditions A syntax-list of conditions that will be combined with the and operator to determine if the loop should continue. These are checked just before the iterations are executed.
action-clauses This is a list of action forms that will be combined with the current-conditions if they are defined, otherwise they go into the body naked.
current-cond-body This is a list of clauses for a cond form. This implements the if and else, and do clauses of the loop. The current-condition gets added to this list along with code from one or more action clauses.
body is a collection of all action clauses and current-cond-bodys to be executed.
list-defs are let-bindings for any lists that are being iterated over using a for clause. They are named with the (gensym) function.
let-defs are let-bindings for any variables bound with a for clause.
3.2 Variables representing identifiers
References to identifiers in the <loop-body> below need to be made from the code in the variables above, and this code is generated outside of the scope of the <loop-body>. Because of Racket’s hygienic macros, the only way to do this is to put the identifiers themselves into variables that have a wider scope. Some of these identifiers are just the names of variables within the local-loop block, but others may be changed during macro expansion:
call-with-cc determines the type of continuation that will be used. This was going to be used to implement a yield clause, which would change it from call/ec (the default) to call/cc, but the racket/generator package already provides a yield form that can be used effectively from within the loop macro, so I’ll save myself the headache of reinventing generators for Racket.
initial-count, initial-sum, initial-string, and initial-collection are initial values for collector variables. They default to #f unless the relevant clause is used. The #f value is used to control the return value of the loop.
These variables can then be combined to form the loop itself, as it will eventually be expanded.:
#`(let ((#,collection #,initial-collection) (#,count* #,initial-count) (#,sum* #,initial-sum) (#,min #f) (#,max #f) (#,string-accumulator #,initial-string) (#,reverse? #,reverse-value) #,@let-defs #,@list-defs) (#,call-with-cc (λ (#,return-continuation) (parameterize ((return-cc #,return-continuation)) (gnarled-let-nest #,gnarled-let-defs (begin #,@prologue #,@initially-prologue (let local-loop () (let-values #,let-values-defs #,@preiterations (unless (and . #,loop-preconditions) <exit-loop>) (begin . #,body) (begin . #,<increment-lists>) (begin . #,iterations) (cond ((and . #,loop-conditions) (local-loop)) (else <exit-loop>))))))))))
(begin #,@epilogue (#,return-continuation (or <generate-collection-type> count sum min max (void))))
The return-continuation is used for all exits from the loop.
All the lists being iterated over are iterated just before all other iterations, including binding of loop variables to the first element of the list. Each list has a corresponding variable that is bound to the next element of the list via the car function. This binding is all that takes place during the iterations, and must happen after the lists themselves have been cdr’d off.
(let unroll-lists ((list-names (get-let-vars list-defs)) (result #'())) (syntax-case list-names () (() result) ((var . rest) (unroll-lists #'rest #`((set! var (cdr var)) . #,result)))))
4 Handling Conditional Statements
Common Lisp’s LOOP facility allows the use of if and else clauses that alter the behavior of the loop. Expansion of these clauses proceeds as follows:
Rewrite all when clauses as if clauses
Put the if foo clause into current-condition, pushing any existing current-condition onto a stack first so that nested ifs can be handled.
If an action clause (such as do, collect, count, etc) is encountered while a current-condition exists, combine the action clause and the current-condition into a clause that can be added to a cond form (current-cond-body). The and operator is added to the front of the current-condition list, unless current-condition is the word else.
After an action clause, an else clause can be encountered, which goes into the current-condition, ultimately adding another clause to the current-cond-body when an action clause is encountered.
If an end clause is encountered, a cond statement is created with the current-cond-body and added to the body.
If an if clause is encountered after if condition action-clause ..., rewrite it as if it was preceded by end.
4.1 Managing a stack of nested if-clauses
(loop for item in list if (something? item) if (something-else? item) do (frobnicate item) else if (third-thing? item) do (replicate item) else (notify item))
The LOOP macro must support syntax like the above, with the effects you would expect based on the indentation shown. This requires managing a stack. If an if clause is encountered while another one is already being processed, the variables pertaining to the existing if must be pushed onto a stack, and popped from that stack when the end of the nested if clause is reached.
(define (push-cond) (set! conditional-stack (cons (list current-cond-body action-clauses current-condition) conditional-stack)) (set-values! (current-cond-body action-clauses current-condition) (values #'() #'() #'()))) (define (pop-cond) (when (null? conditional-stack) (error "END without matching IF clause (may be implicit)")) (set-values! (current-cond-body action-clauses current-condition) (apply values (car conditional-stack))) (set! conditional-stack (cdr conditional-stack)))
4.2 Rewrite when and unless clauses
The chunk of code below is evaluated within the context of a syntax-case macro called loop-body.
((_ (when . rest)) (parse-loop #`(loop-body (if . rest)))) ((_ (unless condition . rest)) (parse-loop #`(loop-body (if (not condition) . rest)))) ((_ (else when . rest)) (parse-loop #`(loop-body (else if . rest)))) ((_ (else unless condition . rest)) (parse-loop #`(loop-body (else if (not condition) . rest))))
4.3 Collect if clauses
((_ (if condition else . rest)) (raise-syntax-error 'if-else "An action clause (such as do, collect, sum, etc) must occur between an if clause and an else clause" #'(if condition else . rest))) ((_ (if condition . rest)) (begin (unless (syntax-null? current-condition) (push-cond)) (set! current-condition #`(condition . #,current-condition)) (set! and? #t) (parse-loop #'(loop-body rest))))
4.4 Collect else and else if clauses
((_ (else if condition . rest)) (begin (set! current-cond-body (add-cond-clause current-condition action-clauses current-cond-body)) (set! current-condition #`(condition)) (set! and? #t) (set! action-clauses #'()) (parse-loop #'(loop-body rest)))) ((_ (else . rest)) (begin (when (syntax-null? current-condition) (raise-syntax-error 'else "else must be preceded by an if, when, or unless followed by action clauses\r\nconnected with 'and'. Example: if condition do condition and collect something" stx)) (set! current-cond-body (add-cond-clause current-condition action-clauses current-cond-body)) (set! current-condition #'else) (set! and? #t) (set! action-clauses #'()) (parse-loop #'(loop-body rest))))
4.5 AND
If AND appears after an action clause, then a subsequent action clause will be part of the previous conditional.
((_ (and . rest)) (begin (set! and? #t) (parse-loop #'(loop-body rest))))
4.6 END
The end clause denotes the end of conditional processing. Action clauses after this will be treated as unconditional, or as belonging to the outer-level if clause. Whatever current-cond-body is being built gets inserted into the body at this point.
((_ (end . rest)) (begin (when (and (syntax-null? current-condition) (syntax-null? current-cond-body)) (raise-syntax-error 'end "end must be preceded by an if, when, or unless clause and an action clause." stx)) (set! and? #f) (unless (syntax-null? current-condition) (set! current-cond-body (add-cond-clause current-condition action-clauses current-cond-body)) (set! action-clauses #'()) (set! current-condition #'())) (cond ((null? conditional-stack) (set! body #`(#,@body (cond . #,(syntax-reverse current-cond-body)))) (set! current-cond-body #'())) (else (let ((cond-body #`(cond . #,(syntax-reverse current-cond-body)))) (pop-cond) (set! action-clauses #`(#,@action-clauses #,cond-body))))) (parse-loop #'(loop-body rest))))
(define-for-syntax (add-cond-clause condition cond-body current-cond-body) #`((#,(fix-current-condition condition) . #,cond-body) . #,current-cond-body)) (define-for-syntax (fix-current-condition condition) (if (syntax-null? condition) #'() (syntax-case condition (else) ((hd . tl) #`(and . #,(syntax-reverse condition))) (else condition))))
5 Action clauses
The action clauses are where the current-condition gets combined with some code to add to a current-cond-body.
Most action clauses only accept one form as an argument, but the do clause is special. Any compound form (ie, one surrounded by parentheses) following the do clause is part of the action clause, and a do without an action clause is illegal. This means the do form cannot be processed by rewriting it into another do form. Instead, it is rewritten as a do-internal form, which is ignored if the form following it is not a compound form.
((_ (do (hd . tl) (hd2 . tl2) . rest)) (parse-loop #'(loop-body (do-internal (hd . tl) and do (hd2 . tl2) . rest)))) ((_ (do (hd . tl) . rest)) (parse-loop #'(loop-body (do-internal (hd . tl) . rest)))) ((_ (do non-list . rest)) (raise-syntax-error 'do "Missing compound-form after do" #'(do non-list . rest)))
5.1 Unavoidable boilerplate
There are various chores that each action clause must accomplish on its own. Unfortunately, macro hygiene makes it impossible to define macros to do it. The macros must be defined in a separate file due to limitations in Racket, and due to the hygiene, local variables here wouldn’t be visible to the code generated by the macro.
One thing that each action clause must do is detect whether two action clauses have been written in a row, which breaks the conditional. For example in the following code:
(loop for x from 1 to 10 if (even? x) collect x do (displayln x))
..the do clause should be interpreted as if it was preceded by an end clause. That is, the cond form that goes with the if clause should be generated before generating the code that implements the do clause. The check for whether this should be done is:
(or and? (syntax-null? current-condition))
The current-condition is the boolean expression (missing its and operator) that determines whether the action-clause should be executed. If there is no current-condition, then that means the action-clause being processed should be executed unconditionally. But if even if there is a current-condition, it may be a stale current-condition left over from a previous action-clause. The current-condition is only fresh if the and? flag is set.
Therefore, if the above expression is true, it is safe to go ahead and process the action clause, otherwise an end clause must be inserted and processed first. The code to do the inserting varies between action clauses just enough to prevent it from being put in a chunk.
After the action-clause needs no further preprocessing, the next thing that must happen is that some code must be generated. What exactly is generated differs between each action-clause, but all action-clause generated code goes into a list, where it is later either placed into a cond form, or added naked to the body.
The generated snippet of code is added with this function:
(define (add-action-clause clause) (set! action-clauses #`(#,@action-clauses #,clause)))
After adding its action-clause code to the action-clauses using add-action-clause, every action-clause must check if the clause is conditional or not. If the clause is conditional (that is, if current-condition is non-empty), then the action-clauses are left alone for later processing, and the body is not modified, as this will be handled in the end clause. But if the action-clause is unconditional, its contents must be added to the body now.
(when (syntax-null? current-condition) (set! body #`(#,@body #,@action-clauses)) (set! action-clauses #'())) (set! and? #f)
((_ (do-internal (hd . tl) . rest)) (begin (cond (<action-boilerplate-guard> (add-action-clause #'(hd . tl)) <action-boilerplate> (parse-loop #'(loop-body (do-internal . rest)))) (else (parse-loop #'(loop-body (end do-internal (hd . tl) . rest)))))))
When do-internal runs out of compound forms, then everything is placed into the body using the boilerplate code.
((_ (do-internal . rest)) (parse-loop #'(loop-body rest)))
5.2 Other Action Clauses
5.2.1 collect
The collect clause tells the loop to store a value in a collection list, which will be returned.
<collect-into> ((_ (collect value . rest)) (cond (<action-boilerplate-guard> (set! initial-collection #''()) (add-action-clause #`(set! #,collection (cons value #,collection))) <action-boilerplate> (parse-loop #'(loop-body rest))) (else (parse-loop #'(loop-body (end collect value . rest)))))) ((_ (collecting value . rest)) (parse-loop #'(loop-body (collect value . rest))))
The loop macro also supports collecting into a specific variable.
(loop for x in list when (odd? x) collect into odds)
Doing this requires a separate version of the above macro. It wasn’t possible to combine the above’s functionality because there’s no way to compare syntax-objects to tell if an identifier that appears in the pattern equals collection or another macro variable.
Furthermore, because the collection variable can be accessed during iteration, and must be a list in the correct order, it is not possible to cons the list in reverse order and then reverse it, as is done with the implicit collector. Instead, adding to the end of the list is done with append, which makes collect into O(n) for each iteration where a collect into occurs. A loop that uses collect into on every iteration could be as slow as O(n2). Therefore, a warning is issued every time collect into is encountered at compile time.
((_ (collect value into collector . rest)) (cond (<action-boilerplate-guard> (displayln (format "***WARNING: ... collect ~a into ... has O(n) performance PER ITERATION. Your program will be EXTREMELY SLOW!" (syntax->datum #'value))) (displayln (format " Use ... cons ~a into ... instead for O(1) performance." (syntax->datum #'value))) (add-action-clause #`(set! collector (append collector (list value)))) <action-boilerplate> (set! let-defs #`((collector '()) . #,let-defs)) (parse-loop #'(loop-body rest))) (else (parse-loop #'(loop-body (end collect value into collector . rest))))))
5.2.2 cons
Because collect into is such a uselessly pathological case in Racket (in contrast with how useful it is in Common Lisp), an extension is provided: cons into operates like collect into, except the resulting list is seen in reverse order. There is no cons without into, and if there was, it’d be a synonym for collect.
((_ (cons value into collector . rest)) (cond (<action-boilerplate-guard> (add-action-clause #`(set! collector (cons value collector))) <action-boilerplate> (set! let-defs #`((collector '()) . #,let-defs)) (parse-loop #'(loop-body rest))) (else (parse-loop #'(loop-body (end cons value into collector . rest)))))) ((_ (consing . rest)) (parse-loop #'(loop-body (cons . rest))))
5.2.3 collect variants
The collect clause can return different types. The return type is controlled by the collection-type variable, which can be set using the with-collection-type clause. When the loop is about to return, it checks the collection-type and constructs a return value as follows:
(begin (if #,collection (case #,collection-type ((list) (if #,reverse? (reverse #,collection) #,collection)) ((vector) (list->vector (reverse #,collection))) ((string) (list->string (reverse #,collection))) ((bytes) (list->bytes (reverse #,collection))) ((hash) (list->hash #,collection)) ((hash/immutable) (list->hash/immutable #,collection))) #f))
That last two conversions are not provided by Racket. They must be implemented here.
(define-syntax define-list->hash (syntax-rules () ((_ list->hash hash-return make pair set) (define (list->hash lst) (call/ec (λ (return) (loop with hash-return = (make) for pair in lst do set finally (return hash-return)))))))) (define-list->hash list->hash hash-return make-hash pair (hash-set! hash-return (car pair) (cdr pair))) (define-list->hash list->hash/immutable hash-return make-immutable-hash pair (set! hash-return (hash-set hash-return (car pair) (cdr pair))))
((_ (with-collection-type type . rest)) (begin (case (syntax->datum #'type) ((list) #t) ((vector) #t) ((string) #t) ((bytes) #t) ((hash) #t) ((hash/immutable) #t) (else (raise-syntax-error 'with-collection-type "Unsupported collection type" #'type))) (set! collection-type #`'type) (parse-loop #'(loop-body rest))))
5.2.4 append
This is like collect, except the value must be a list, which will be appended to the end of the collection.
((_ (append value into collector . rest)) (cond (<action-boilerplate-guard> (add-action-clause #`(set! collector (append collector value))) <action-boilerplate> (set! let-defs #`((collector '()) . #,let-defs)) (parse-loop #'(loop-body rest))) (else (parse-loop #'(loop-body (end append value into collector . rest)))))) ((_ (append value . rest)) (cond (<action-boilerplate-guard> (set! initial-collection #''()) (add-action-clause #`(loop for item in value do (set! #,collection (cons item #,collection)))) <action-boilerplate> (parse-loop #'(loop-body rest))) (else (parse-loop #'(loop-body (end append value . rest)))))) ((_ (appending . rest)) (parse-loop #'(loop-body (append . rest))))
5.2.5 sum
This clause adds the given value to a numerical accumulator, which is then returned.
((_ (sum value into collector . rest)) (cond (<action-boilerplate-guard> (add-action-clause #`(set! collector (+ collector value))) <action-boilerplate> (set! let-defs #`((collector 0) . #,let-defs)) (parse-loop #'(loop-body rest))) (else (parse-loop #'(loop-body (end sum value into collector . rest)))))) ((_ (sum value . rest)) (cond (<action-boilerplate-guard> (set! initial-sum #'0) (add-action-clause #`(set! #,sum* (+ #,sum* value))) <action-boilerplate> (parse-loop #'(loop-body rest))) (else (parse-loop #'(loop-body (end sum value . rest)))))) ((_ (summing . rest)) (parse-loop #'(loop-body sum . rest)))
5.2.6 count
Counts the number of times the expression evaluates as true.
((_ (count expression into collector . rest)) (cond (<action-boilerplate-guard> (add-action-clause #`(when expression (set! collector (add1 collector)))) <action-boilerplate> (set! let-defs #`((collector 0) . #,let-defs)) (parse-loop #'(loop-body rest))) (else (parse-loop #'(loop-body (end count expression into collector . rest)))))) ((_ (count expression . rest)) (cond (<action-boilerplate-guard> (set! initial-count #'0) (add-action-clause #`(when expression (set! #,count* (add1 count)))) <action-boilerplate> (parse-loop #'(loop-body rest))) (else (parse-loop #'(loop-body end count expression . rest))))) ((_ (counting . rest)) (parse-loop #'(loop-body (count . rest))))
5.2.7 minimize and maximize
This binds the smallest random number seen into min-random:
(loop repeat 100 minimizing (random 100) into min-random ...)
((_ (minimize expression into collector . rest)) (cond (<action-boilerplate-guard> (add-action-clause #`(let ((temp expression)) (when (or (not collector) (< temp collector)) (set! collector temp)))) <action-boilerplate> (set! let-defs #`((collector #f) . #,let-defs)) (parse-loop #'(loop-body rest))) (else (parse-loop #'(loop-body (end minimize expression into collector . rest)))))) ((_ (minimize expression . rest)) (cond (<action-boilerplate-guard> (add-action-clause #`(let ((temp expression)) (when (or (not min) (< temp min)) (set! min temp)))) <action-boilerplate> (parse-loop #'(loop-body rest))) (else (parse-loop #'(loop-body end count expression . rest))))) ((_ (minimizing . rest)) (parse-loop #'(loop-body (minimize . rest)))) ((_ (maximize expression into collector . rest)) (cond (<action-boilerplate-guard> (add-action-clause #`(let ((temp expression)) (when (or (not collector) (> temp collector)) (set! collector temp)))) <action-boilerplate> (set! let-defs #`((collector #f) . #,let-defs)) (parse-loop #'(loop-body rest))) (else (parse-loop #'(loop-body (end count expression into collector . rest)))))) ((_ (maximize expression . rest)) (cond (<action-boilerplate-guard> (add-action-clause #`(let ((temp expression)) (when (or (not max) (> temp max)) (set! max temp)))) <action-boilerplate> (parse-loop #'(loop-body rest))) (else (parse-loop #'(loop-body end count expression . rest))))) ((_ (maximizing . rest)) (parse-loop #'(loop-body (maximize . rest))))
6 While and Until
(loop while keep-going ...)
(loop until stop ...)
((_ (while condition . rest)) (begin (set! loop-preconditions #`(condition . #,loop-preconditions)) (parse-loop #'(loop-body rest)))) ((_ (until condition . rest)) (parse-loop #'(loop-body (while (not condition) . rest))))
7 Repeating a Set Number of Times
(loop repeat 15 collect 'ocd)
(ocd ocd ocd ocd ocd ocd ocd ocd ocd ocd ocd ocd ocd ocd ocd)
((_ (repeat n . rest)) (parse-loop #'(loop-body (for x from 1 to n . rest))))
8 Iterating over stuff
The for keyword denotes all forms of iteration:
(loop for variable preposition some-kind-of-collection ...)
In traditional Common Lisp, the preposition determines the type of some-kind-of-collection:
in to iterate over lists
across to iterate over arrays
In Common Lisp, strings and vectors are both arrays, and Common Lisp has no equivalent to bytes.
Common Lisp also provides the on preposition, which iterates over lists, except that the variable is set to the entire remaining portion of the list instead of just the next element in the list.
In this version of the loop macro, across iterates over vectors, strings, and bytes, while in iterates over lists and hash tables, and over iterates over generators.
This version of the macro also iterates over hash-tables and generators.
All the variants of the for-clause can be captured by this syntax-case pattern:
((_ (for . rest)) (unless (and (syntax-null? current-condition) (syntax-null? action-clauses) (syntax-null? current-cond-body)) (raise-syntax-error 'loop "\"for\" must precede all \"if\", \"when\", \"collect\", and \"do\" clauses" stx)) (syntax-case #'(for . rest) <loop-literals> <individual-for-clauses> (not-a-for-clause (parse-loop #'(loop-body not-a-for-clause)))))
The variations are all processed in a local syntax-case form.
8.1 for x in y: List and Hash Iteration
Iterating over lists is the most basic case. The list-defs let-bindings can hold a binding for the list y, which automatically results in the list being cdr’d as the loop progresses, while the iterator variable x is part of the let-defs. Finally, iterations receives code that will update x with each loop iteration.
<destructuring-for-x-in-y> ((for x in y by next . rest) (let ((y-binding (datum->syntax stx (gensym))) (next-binding (datum->syntax stx (gensym)))) (set! iterations #`((set! x #f) (set! #,y-binding (#,next-binding #,y-binding)) . #,iterations)) (set! preiterations #`(#,@preiterations (set! x (if (null? #,y-binding) #f (car #,y-binding))))) (set! loop-preconditions #`((not (null? #,y-binding)) . #,loop-preconditions)) (set! loop-conditions #`((not (null? #,y-binding)) . #,loop-conditions)) (parse-loop #`(loop-body (with #,next-binding = next with #,y-binding = y with x = (if (null? #,y-binding) #f (car #,y-binding)) . rest))))) ((for x in y . rest) (parse-loop #`(loop-body (for x in y by cdr . rest))))
Common Lisp also supports the following:
(loop for ((a b) (c d)) in '(((1 2) (3 4)) ((5 6) (7 8))) ...)
...where the elements in the lists can be destructured according to the pattern ((a b) (c d)) to arbitrary depth. The variables can be bound using Racket’s old mzlib/match, (but not the racket/match, due to the need to throw in the word list gratuitously). For some reason, you cannot import racket/mzlib into a Scribble/LP program, due specifically to conflicts between MzLib’s match and Racket’s match. This error doesn’t occur under #lang racket. For this reason, a macro, destructuring-bind, had to be defined in a separate file. destructuring-bind is used exactly like its Common Lisp counterpart.
((for (x . rest-of-pattern) in y . rest) (let* ((y-binding (datum->syntax stx (gensym))) (variables <extract-variables-from-pattern>) (condition #`(not (null? #,y-binding)))) (set! preiterations #`(#,@preiterations (when #,condition (set-values! #,variables (destructuring-bind (x . rest-of-pattern) (car #,y-binding) (values . #,variables)))))) (set! loop-preconditions #`(#,condition . #,loop-preconditions)) (set! loop-conditions #`((not (null? #,y-binding)) . #,loop-conditions)) (set! list-defs #`((#,y-binding y) . #,list-defs)) (set! let-values-defs #`((#,variables (destructuring-bind (x . rest-of-pattern) (car #,y-binding) (values . #,variables))) . #,let-values-defs)) (parse-loop #'(loop-body rest))))
Before it becomes possible to bind variables specified in this pattern, it is necessary to flatten the pattern into a plain list of variable names. This is used both in the let-values form, and in the return value from the match pattern.
(let loop ((pat #'(x . rest-of-pattern)) (result #'())) (syntax-case pat () (() result) (((x . more-vars) . rest) (loop #'rest (loop #'(x . more-vars) result))) ((x . rest) (loop #'rest #`(x . #,result))) (x (loop #'() #`(x . #,result)))))
8.2 for x on y by iterator: List iteration with entire lists.
((for x on y by iter . rest) (let ((y-binding (datum->syntax stx (gensym))) (iter-binding (datum->syntax stx (gensym)))) (set! iterations #`((set! x #f) (set! #,y-binding (#,iter-binding #,y-binding)) . #,iterations)) (set! preiterations #`(#,@preiterations (set! x #,y-binding))) (set! loop-preconditions #`((not (null? #,y-binding)) . #,loop-preconditions)) (set! loop-conditions #`((not (null? #,y-binding)) . #,loop-conditions)) (parse-loop #`(loop-body (with #,y-binding = y and #,iter-binding = iter with x = #,y-binding . rest))))) ((for x on y . rest) (parse-loop #'(loop-body (for x on y by cdr . rest))))
8.3 for x being the hash-keys in table: Hash iteration
This binds var to each of the keys in the hash-table in succession:
(loop for var being the hash-keys in hash-table ...)
You can bind the corresponding hash value to another variable like this:
(loop for var being the hash-keys in hash-table using (hash-value other-var) ...)
The reverse is also supported:
(loop for var being each hash-value in hash-table using (hash-key other-var) ...)
And the following extension is supported:
(loop for (key val) being the hash-pairs in hash-table ...)
each and the are interchangeable, as are the singular/plural forms of hash-keys, etc.
Iterating over hash tables is more difficult. Racket provides no way to get the "next" key and value pair from a hash and remove it. Instead, it provides full-iteration functions such as hash-map and hash-for-each.
A hash table can be rewritten as a list using hash->list, but that would be a bad thing to do if the hash was big.
The hash-for-each function can be used to create a generator, however, and the loop macro can iterate over generators. A generator using hash-for-each will return (void) when iteration completes, but the loop macro requires end-of-generator, because (void) is ambiguous. So the clause is rewritten as an iteration over a generator.
Since Racket’s hash iteration functions always provide both key and value, it makes sense to implement the hash-pairs extension first. The singular hash-pair can be used also, but it is simply rewritten as hash-pairs.
<for-hash-keys> <for-hash-values> ((for (key value) being the hash-pairs in hash . rest) (parse-loop #`(loop-body (for (key value) over (make-hash-generator hash) . rest)))) ((for (key value) being the hash-pair in hash . rest) (parse-loop #'(loop-body (for (key value) being the hash-pairs in hash . rest)))) ((for something being each . rest) (parse-loop #'(loop-body (for something being the . rest))))
The generator is defined like this:
(define (make-hash-generator hash) (generator () (begin (hash-for-each hash (λ (k v) (yield k v))) (yield end-of-generator end-of-generator))))
8.3.1 for x being the hash-keys in hash using...
All of the standard Common Lisp variants for iterating over a hash table are implemented in terms of the variant above.
((for key being the hash-keys in hash using (hash-value value) . rest) (parse-loop #'(loop-body (for (key value) being the hash-pairs in hash . rest)))) ((for key being the hash-keys in hash . rest) (parse-loop #'(loop-body (for (key value) being the hash-pairs in hash . rest)))) ((for k being the hash-key in hash . rest) (parse-loop #'(loop-body for k being the hash-keys in hash . rest)))
((for val being the hash-values in hash using (hash-key key) . rest) (parse-loop #'(loop-body (for (key value) being the hash-pairs in hash . rest)))) ((for val being the hash-values in hash . rest) (parse-loop #'(loop-body (for (key value) being the hash-pairs in hash . rest)))) ((for val being the hash-value . rest) (parse-loop #'(loop-body (for val being the hash-values . rest))))
8.4 for x over y: Generator iteration
For generator iteration, multiple values from (yield) are supported. The loop terminates when the first of these values (or the only value) is end-of-generator, whose value is defined in this file.
Example:
(loop for value over (generator () (for-each (λ (value) (yield value)) '(a b c d e f g)) end-of-generator))
((for (x . rest-vars) over y . rest) (let ((y-binding (datum->syntax stx (gensym)))) (set! let-values-defs #`(((x . rest-vars) (#,y-binding)) . #,let-values-defs)) (set! let-defs #`((#,y-binding y) . #,let-defs)) (let set-precondition-loop ((variables #'(x . rest-vars))) (syntax-case variables () (() #t) ((x . rest-vars) (begin (set! loop-preconditions #`((not (end-of-generator? x)) . #,loop-preconditions)) (set-precondition-loop #'rest-vars))))) (parse-loop #'(loop-body rest)))) ((for x over y . rest) (parse-loop #'(loop-body (for (x) over y . rest))))
8.5 for x across y: Vector, string, and byte iteration
((for x across y . rest) (let* ((y-binding (datum->syntax stx (gensym))) (yix (datum->syntax stx (gensym))) (loop-condition #`(< #,yix (alen #,y-binding)))) (set! let-defs #`((x #f) (#,yix 0) (#,y-binding y) . #,let-defs)) (set! preiterations #`(#,@preiterations (when #,loop-condition (set! x (aref #,y-binding #,yix))))) (set! loop-preconditions #`(#,loop-condition . #,loop-preconditions)) (set! iterations #`((set! #,yix (add1 #,yix)) . #,iterations)) (parse-loop #`(loop-body (with #,y-binding = y and #,yix = 0 with x = (if (>= #,yix (alen #,y-binding)) #f (aref #,y-binding #,yix)) . rest)))))
8.6 for x = y: Iterating over numbers
<for-x=y-then> ((for x = y . rest) (begin (set! let-defs #`((x #f) . #,let-defs)) (set! preiterations #`(#,@preiterations (set! x y))) (parse-loop #'(loop-body rest))))
((for x = y then step-form . rest) (begin (set! let-defs #`((x #f) . #,let-defs)) (set! prologue #`(#,@prologue (set! x y))) (set! iterations #`((set! x step-form) . #,iterations)) (parse-loop #'(loop-body rest))))
<for-x-from-low-to-high> ((for x from low by step . rest) (let ((step-binding (datum->syntax stx (gensym)))) (set! let-defs #`((#,step-binding step) . #,let-defs)) (parse-loop #`(loop-body (for x = low then (+ x #,step-binding) . rest))))) ((for x from low . rest) (begin (parse-loop #'(loop-body (for x from low by 1 . rest))))) ((for x downfrom high . rest) (parse-loop #'(loop-body (for x from high by -1 . rest))))
((for x from low to high by step . rest) (let ((high-binding (datum->syntax stx (gensym)))) (set! let-defs #`((#,high-binding #f) . #,let-defs)) (set! prologue #`(#,@prologue (set! #,high-binding high))) (set! loop-preconditions #`((<= x #,high-binding) . #,loop-preconditions)) (parse-loop #'(loop-body (for x from low by step . rest))))) ((for x from low to high . rest) (begin (parse-loop #'(loop-body (for x from low to high by 1 . rest))))) ((for x from low below high by step . rest) (let ((high-binding (datum->syntax stx (gensym)))) (set! let-defs #`((#,high-binding #f) . #,let-defs)) (set! prologue #`(#,@prologue (set! #,high-binding high))) (set! loop-preconditions #`((< x #,high-binding) . #,loop-preconditions)) (parse-loop #'(loop-body (for x from low by step . rest))))) ((for x from low below high . rest) (begin (parse-loop #'(loop-body (for x from low below high by 1 . rest))))) ((for x from low upto high . rest) (begin (parse-loop #'(loop-body (for x from low to high . rest))))) ((for x from high downto low by step . rest) (let ((low-binding (datum->syntax stx (gensym)))) (set! let-defs #`((#,low-binding #f) . #,let-defs)) (set! prologue #`(#,@prologue (set! #,low-binding low))) (set! loop-preconditions #`((>= x #,low-binding) . #,loop-preconditions)) (parse-loop #`(loop-body (for x = high then (- x step) . rest))))) ((for x from high downto low . rest) (begin (parse-loop #'(loop-body (for x from high downto low by 1 . rest))))) ((for x downfrom high to low . rest) (parse-loop #'(loop-body (for x from high downto low . rest)))) ((for x from high above low by step) (let ((low-binding (datum->syntax stx (gensym)))) (set! let-defs #`((#,low-binding #f) . #,let-defs)) (set! prologue #`(#,@prologue (set! #,low-binding low))) (set! loop-preconditions #`((> x #,low-binding) . #,loop-preconditions)) (parse-loop #`(loop-body (for x = high then (- x step) . rest))))) ((for x from high above low . rest) (begin (parse-loop #'(loop-body (for x from high above low by 1)))))
9 WITH: Binding variables
With is used like this:
(loop with x = value ...)
It binds x to the given value by wrapping everything in a let* form. There is a variant:
(loop with x = value and y = other-value)
This variant wraps using a let form instead of let*. Of course the two variants can be mixed, producing a gnarled nest of let and let* forms over the body of the loop.
((_ (with x = value and y = other-value . rest)) (begin (set! current-gnarled-let-def #`(#,@current-gnarled-let-def (y other-value))) (parse-loop #'(loop-body (with* x = value . rest))))) ((_ (with* x = value and y = other-value . rest)) (parse-loop #'(loop-body (with x = value and y = other-value . rest)))) ((_ (with* x = value . rest)) (begin (set! gnarled-let-defs #`(#,@gnarled-let-defs (#,@current-gnarled-let-def (x value)))) (set! current-gnarled-let-def #'()) (parse-loop #'(loop-body rest)))) ((_ (with x = value . rest)) (begin (set! gnarled-let-defs #`(#,@gnarled-let-defs * ((x value)))) (parse-loop #'(loop-body rest))))
10 FINALLY
The finally clause executes at the end of iteration.
((_ (finally form . rest)) (begin (set! epilogue #`(#,@epilogue form)) (parse-loop #'(loop-body rest))))
11 INITIALLY
The initially clauses execute at the beginning of iteration, just after all variables have been initialized.
((_ (initially form . rest)) (begin (set! initially-prologue #`(#,@initially-prologue form)) (parse-loop #'(loop-body rest))))
12 always, never, and thereis
((_ (thereis form . rest)) (let ((success? (datum->syntax stx (gensym)))) (set! let-defs #`((#,success? #f) . #,let-defs)) (set! body #`((when form (set! #,success? #t) (return #t)) . #,body)) (parse-loop #`(loop-body (finally (return #,success?) . rest))))) ((_ (always form . rest)) (begin (let ((success? (datum->syntax stx (gensym)))) (set! let-defs #`((#,success? #t) . #,let-defs)) (set! body #`((when (not form) (set! #,success? #f) (return #f)) . #,body)) (parse-loop #`(loop-body (finally (return #,success?) . rest)))))) ((_ (never form . rest)) (parse-loop #'(loop-body (always (not form) . rest))))
13 Fixing the Mistake That the R6RS Committee Made
Racket loosely follows R6RS, which states that syntax literals, such as the ample number used in the implementation of this macro, must refer to bindings, which can be overridden. They did this with full awareness that doing this makes it possible to break the basic syntax of the language. For example (and this example is used by the R6RS committee to specify what Scheme should do), the definition of else below breaks the cond form that follows it:
> (define else #f)
> (cond (#f 'not-this) (else 'should-return-this))
The LOOP macro has a lot of literal keywords, and I’ve added a few of my own. One of these, count, is already overridden by Racket’s library, but not by Scribble/LP, resulting in count not being able to be recognized as a loop keyword from Racket. This program would produce a syntax error:
(loop count #t do (return))
Furthermore, it would be easy for someone to attempt to use this library along with another library that binds words like from or with to something, and then they wouldn’t be recognizable as keywords when used in this macro. That would be very undesireable.
Also undesireable would be the result of following the advice given to me by Racket’s developers. They suggested that I bind every single one of these keywords:
(for by as being by the each hash-key hash-keys hash-value hash-values hash-pair hash-pairs from while do do-internal collect collecting repeat repeating with with* sum summing append then appending matching nconc nconcing cons consing count counting string-append minimize minimizing maximize maximizing below above to downto downfrom upto in into on across over = until always never thereis and end else named initially finally if when unless return with-collection-type)
to a value or macro and then export them. The macro would still break if you required a library that has its own bindings to those words, if you were even able to require both libraries at all and still have a program that compiles.
Fortunately, it seems that Racket’s devs have run into this problem before, and I stumbled onto the syntax-case* form, which allows you to specify your own procedure to compare symbols for the purpose of pattern matching. The procedure I created for this considers two symbols to be equal if they look equal to the naked eye:
(define-for-syntax (stx-compare stx-1 stx-2) (eq? (syntax->datum stx-1) (syntax->datum stx-2)))
(define-syntax loop-body (λ (stx) <expansion-variables> <local-expander-functions> (let parse-loop ((stx stx)) (define first-word (syntax-case stx () ((_ ()) #f) ((_ (first . rest)) (syntax->datum #'first)))) (syntax-case* stx <loop-literals> stx-compare ((_ ()) (cond (<action-boilerplate-guard> (let ((let-vars (get-let-vars let-defs))) <loop-body>)) (else (parse-loop #'(loop-body (end)))))) <with> <initially-clause> <finally> <always/never/thereis> <rewrite-if-clauses> <collect-if-clauses> <collect-else-if> <while/until> <repeat> <and> <end> <do-clause> <do-internal> <do-internal/2> <collect-clause> <min/max> <cons-into> <with-collection-type> <append-clause> <sum-clause> <count-clause> <universal-for-clause>)))) <outer-loop-macro>
14 The Outer Loop Macro
The outer loop macro is the macro that is directly used by the user. It expands to either the inner loop macro, called loop-body, or to an optimized form. For example, if the programmer writes this:
(loop for item in a-list collect (do-something-to item))
instead of expanding to the loop body seen above, it simply expands to this:
(call/ec (λ (ec) (parameterize ((return-cc ec)) (map (λ (item) (do-something-to item)) a-list))))
The call/ec is necessary because the macro cannot prove that you’re not doing this:
(loop for item in a-list collect (if (good? item) item (return 'bad-item-found!)))
...and if you are doing that, the call/ec is required for it to work. The for optimization is used for any number of for clauses as long as there is only one collect clause and it occurs at the end. The additional for clauses result in more list arguments being passed to map, and more arguments being accepted by the lambda. Special care must be taken not to match against the destructuring version of the for-loop, (loop for (x (y z)) in a-list collect something), since that requires special treatment to get the destructuring part to work.
(begin-for-syntax (define (all-for-x-in-y/collect? stx) (cond ((syntax-null? stx) #f) (else (let local-loop ((clauses stx)) (syntax-case* clauses (for in collect) stx-compare ((for x in (hd . tl) . rest) #f) ((for x in y collect z) #t) ((for x in y . rest) (local-loop #'rest)) (_ #f)))))) (define (expand-only-for-x-in-y/collect stx) (let local-loop ((clauses stx) (lambda-args #'()) (lists #'())) (syntax-case* clauses (for in collect) stx-compare ((for x in y collect z) #`(call/ec (λ (ec) (parameterize ((return-cc ec)) (map (λ (x . #,lambda-args) z) y . #,lists))))) ((for x in y . rest) (local-loop #'rest #`(x . #,lambda-args) #`(y . #,lists))))))) (define-syntax loop (λ (stx) (syntax-case* stx () stx-compare ((_ . body) (cond ((all-for-x-in-y/collect? #'body) (expand-only-for-x-in-y/collect #'body)) (else (syntax-case* stx <loop-literals> stx-compare ((_ named block-name . body) #'(call/ec (λ (block-name) (loop . body)))) ((_ . body) #'(loop-body body))))))))) (provide loop)
(define-syntax unwind-protect (syntax-rules () ((_ value-form cleanup-form) (dynamic-wind (let ((ok? #t)) (lambda () (if ok? (set! ok? #f) (error "Re-entering UNWIND-PROTECT is prohibited.")))) (lambda () value-form) (lambda () cleanup-form))))) (define-syntax gnarled-let-nest (syntax-rules (*) ((_ () . body) (begin . body)) ((_ (* (bindings ...) . more-bindings) . body) (let* (bindings ...) (gnarled-let-nest more-bindings . body))) ((_ ((bindings ...) . more-bindings) . body) (let (bindings ...) (gnarled-let-nest more-bindings . body))) ((_ (* (bindings ...)) . body) (let* (bindings ...) . body)) ((_ ((bindings ...)) . body) (let (bindings ...) . body))))
(define-for-syntax (syntax-null? stx) (syntax-case stx () (() #t) (_ #f))) (define-for-syntax (syntax-reverse stx) (let loop ((rest stx) (result #'())) (syntax-case rest () (() result) ((hd . tl) (loop #'tl #`(hd . #,result)))))) (define-for-syntax (get-let-vars stx) (let loop ((rest stx) (result #'())) (syntax-case rest () (() (syntax-reverse result)) (((var . value) . rest) (loop #'rest #`(var . #,result)))))) (define-for-syntax (syntax-find pred? syntax-list) (let loop ((rest syntax-list)) (syntax-case rest () (() #f) ((hd . tl) (if (pred? #'hd) #'hd (loop #'tl)))))) (define-for-syntax (add-iterations let-vars iterations) (let loop ((let-vars let-vars) (iterations iterations) (result #'())) (syntax-case let-vars () (() (syntax-reverse result)) ((var . rest) (let ((iter (syntax-find (λ (stx) (syntax-case stx () ((var2 . fuckit) (eq? (syntax->datum #'var2) (syntax->datum #'var))))) iterations))) (loop #'rest iterations (if iter (syntax-case iter () ((var body) #`(body . #,result))) #`(var . #,result)))))))) (define (aref arr n) ((cond ((vector? arr) vector-ref) ((string? arr) string-ref) ((bytes? arr) bytes-ref)) arr n)) (define (alen arr) ((cond ((vector? arr) vector-length) ((string? arr) string-length) ((bytes? arr) bytes-length)) arr)) <stx-compare> (define-for-syntax (print-syntax stx) (displayln (syntax->datum stx))) <list->hash>