The Common Lisp LOOP Macro for Racket
(See also: http://www.gigamonkeys.com/book/loop-for-black-belts.html)
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.
1 A Few of the Things the LOOP Can Do
It can implement simple while and until loops like those seen in other programming languages, while also having its own local variables:
> (require "main.rkt")
> (loop with x = 0 while (< x 3) do (displayln x) (set! x (add1 x)))
0
1
2
...and it can implement the classic BASIC FOR loop
> (require "main.rkt")
> (loop for line from 10 to 40 by 10 do (displayln (format "~a PRINT \"LOOK AROUND YOU! \";" line)))
10 PRINT "LOOK AROUND YOU! ";
20 PRINT "LOOK AROUND YOU! ";
30 PRINT "LOOK AROUND YOU! ";
40 PRINT "LOOK AROUND YOU! ";
...and it can do exactly the same thing a number of times:
> (require "main.rkt")
> (loop repeat 2 do (displayln 'Deja-Vu!))
Deja-Vu!
Deja-Vu!
But it can also iterate over lists while simutaneously incrementing counters and filtering on either one, returning a list.
> (require "main.rkt")
> (loop for x in '(a b c d e f g) for y from 0 when (even? y) collect x) '(a c e g)
Though the syntax resembles that of for/list, LOOP’s when clause does not cause a nested loop to begin. In fact, additional for clauses are illegal after the when clause.
It can iterate over multiple lists simultaneously and collect the results in a hash table:
> (require "main.rkt")
> (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) (f . 6) (c . 3) (b . 2) (a . 1) (e . 5) (d . 4))
(The supported collection types are list (default), vector, string, bytes, hash, and hash/immutable.)
Using the across iteration keyword, it can iterate over strings, vectors, and bytes:
> (require "main.rkt")
> (loop for char across "abcdefλ" for byte across #"abcdefg" for number across (loop repeat 7 collect (random 512) with-collection-type vector) for counter from 1 with-collection-type hash/immutable collect (cons (format "char-~a" counter) char) collect (cons (format "byte-~a" counter) byte) collect (cons (format "number-~a" counter) number))
'#hash(("number-5" . 436)
("number-7" . 348)
("char-6" . #\f)
("char-3" . #\c)
("byte-7" . 103)
("number-1" . 474)
("byte-3" . 99)
("char-7" . #\λ)
("number-2" . 311)
("byte-5" . 101)
("char-5" . #\e)
("number-4" . 344)
("byte-1" . 97)
("number-3" . 456)
("byte-4" . 100)
("char-4" . #\d)
("char-2" . #\b)
("byte-2" . 98)
("number-6" . 221)
("char-1" . #\a)
("byte-6" . 102))
It can filter into multiple output collections on differing criteria (you can only change the collection type of unnamed collections, though):
> (require "main.rkt")
> (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)))))
> (sift symbol? '(a b c 3 1 9 #\j #\z d #\o 38 e (some random list) f g))
'(a b c d e f g)
'(3 1 9 #\j #\z #\o 38 (some random list))
It can iterate over generators using the over iteration keyword:
> (require "main.rkt")
> (require racket/generator)
> (define gen (generator () (loop for v = 2 then (expt v 2) do (yield v))))
> (loop for bignum over gen repeat 11 do (displayln bignum))
2
4
16
256
65536
4294967296
18446744073709551616
340282366920938463463374607431768211456
115792089237316195423570985008687907853269984665640564039457584007913129639936
13407807929942597099574024998205846127479365820592393377723561443721764030073546976801874298166903427690031858186486050853753882811946569946433649006084096
179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216
...and hash tables
> (require "main.rkt")
> (loop for (file test-status) being the hash-pairs in '#hash((file1.rkt . pass) (badfile.rkt . fail) (goodfile.rkt . pass)) if (eq? test-status 'fail) collect file) '(badfile.rkt)
It can count:
> (require "main.rkt")
> (require math/number-theory)
> (loop for n from 0 to 1000 for fib = (fibonacci n) count (even? fib)) 334
...more than one thing at a time, while simultaneously finding a minimum or maximum number:
> (require "main.rkt")
> (require math/number-theory)
> (loop for n from 0 to 1000 for fib = (fibonacci n) count (even? fib) into evens count (prime? fib) into primes count (square-number? fib) into squares when (square-number? fib) maximize fib into biggest-square finally (return `((square numbers = ,squares) (prime numbers = ,primes) (largest square = ,biggest-square) (even numbers = ,evens))))
'((square numbers = 4)
(prime numbers = 21)
(largest square = 144)
(even numbers = 334))
...and it can exit a multi-level-deep loop:
> (require "main.rkt")
> (loop named outer for a-list in '((1 2 3 4 5 6 7 8 9) (10 11 12 13 14 15) (16 '17 18 19 20 21 22) (23 24 25 26 27 28 29)) do (loop for n in a-list unless (number? n) do (return-from outer n))) ''17
It can bind into the structure of the elements in a list. Also, the conditional clauses can be nested:
> (require "main.rkt")
> (define test (loop for x from 1 to 10 for y downfrom 10 to 1 collect (cons x y)))
> test
'((1 . 10)
(2 . 9)
(3 . 8)
(4 . 7)
(5 . 6)
(6 . 5)
(7 . 4)
(8 . 3)
(9 . 2)
(10 . 1))
> (loop for (x . y) in test if (> x y) if (even? y) collect y else collect (list 'odd y) end else collect 'x-too-small)
'(x-too-small
x-too-small
x-too-small
x-too-small
x-too-small
(odd 5)
4
(odd 3)
2
(odd 1))
2 Pattern matching
Pattern-matching on list values is supported in all assignments:
> (require "main.rkt")
> (apply string-append (loop for (token next-token) on '("separated" "by" "commas" "except" "at" "the" "end") collect token when next-token collect ",")) "separated,by,commas,except,at,the,end"
> (require "main.rkt")
> (loop with (x y . z) = '(1 2 3 4 5) do (return (cons (+ x y) z))) '(3 3 4 5)
The pattern matcher is permissive, being able to match a large number of variables to a short list:
> (require "main.rkt")
> (loop with (a b c d (e1 e2 e3) f g) = '(1 2 3) do (return (list a b c d (list e1 e2 e3) f g))) '(1 2 3 #f (#f #f #f) #f #f)
3 Comparison with Racket’s for loops
> (require "main.rkt")
> (for ([i '(1 2 3)] [j "abc"] #:when (odd? i) [k #(#t #f)]) (displayln (list i j k)))
(1 a #t)
(1 a #f)
(3 c #t)
(3 c #f)
> (loop for i in '(1 2 3) for j across "abc" when (odd? i) do (loop for k across #(#t #f) do (displayln (list i j k))))
(1 a #t)
(1 a #f)
(3 c #t)
(3 c #f)
> (require "main.rkt")
> (for ([(i j) #hash(("a" . 1) ("b" . 20))]) (display (list i j))) (a 1)(b 20)
> (loop for (i j) being each hash-pair in #hash(("a" . 1) ("b" . 20)) do (display (list i j))) (a 1)(b 20)
> (for ([i '(1 2 3)] [j "abc"] #:break (not (odd? i)) [k #(#t #f)]) (display (list i j k))) (1 a #t)(1 a #f)
> (loop for i in '(1 2 3) for j across "abc" while (odd? i) do (loop for k across #(#t #f) do (display (list i j k)))) (1 a #t)(1 a #f)
> (for/list ([i '(1 2 3)] [j "abc"] #:when (odd? i) [k #(#t #f)]) (list i j k)) '((1 #\a #t) (1 #\a #f) (3 #\c #t) (3 #\c #f))
> (loop for i in '(1 2 3) for j across "abc" when (odd? i) append (loop for k across #(#t #f) collect (list i j k))) '((1 #\a #t) (1 #\a #f) (3 #\c #t) (3 #\c #f))
> (for/vector ([i '(1 2 3)]) (number->string i)) '#("1" "2" "3")
> (loop for i in '(1 2 3) with-collection-type vector collect (number->string i)) '#("1" "2" "3")
> (for/vector #:length 2 ([i '(1 2 3)]) (number->string i)) '#("1" "2")
> (loop for i in '(1 2 3) repeat 2 with-collection-type vector collect (number->string i)) '#("1" "2")
> (for/hash ((i '(1 2 3))) (values i (number->string i))) '#hash((1 . "1") (2 . "2") (3 . "3"))
> (loop for i in '(1 2 3) with-collection-type hash collect (cons i (number->string i))) '#hash((3 . "3") (2 . "2") (1 . "1"))
4 The pathological case of collect ... into
Common Lisp allows you to do this:
(loop for x from 1 to 10 collect (* x 2) into doubled finally (return doubled))
Unfortunately, doing that without the loop taking exponential time requires mutating the final cdr of doubled, which is not allowed in Racket. Therefore, this is implemented with append, which has to cons up a brand new list every time, which will take progressively longer as doubled gets longer. A workaround is provided:
> (require "main.rkt")
> (loop for x from 1 to 10 cons (* x 2) into doubled finally (return (reverse doubled))) '(2 4 6 8 10 12 14 16 18 20)
Furthermore, a warning is issued at compile time if you write or port code that uses the collect ... into clause:
> (require "main.rkt")
> (loop for x from 1 to 10 collect (* x 2) into doubled finally (return doubled))
***WARNING: ... collect (* x 2) into ... has O(n) performance PER ITERATION. Your program will be EXTREMELY SLOW!
Use ... cons (* x 2) into ... instead for O(1) performance.
'(2 4 6 8 10 12 14 16 18 20)