The Common Lisp LOOP Macro for Racket
1 A Few of the Things the LOOP Can Do
2 Pattern matching
3 Comparison with Racket’s for loops
4 The pathological case of collect ... into
6.1

The Common Lisp LOOP Macro for Racket

(See also: http://www.gigamonkeys.com/book/loop-for-black-belts.html)

    1 A Few of the Things the LOOP Can Do

    2 Pattern matching

    3 Comparison with Racket’s for loops

    4 The pathological case of collect ... into

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((c . 3) (b . 2) (a . 1) (e . 5) (d . 4) (g . 7) (f . 6))

(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" . 398)

       ("number-7" . 308)

       ("char-6" . #\f)

       ("char-3" . #\c)

       ("byte-7" . 103)

       ("number-1" . 23)

       ("byte-3" . 99)

       ("char-7" . #\λ)

       ("number-2" . 294)

       ("byte-5" . 101)

       ("char-5" . #\e)

       ("number-4" . 362)

       ("byte-1" . 97)

       ("number-3" . 46)

       ("byte-4" . 100)

       ("char-4" . #\d)

       ("char-2" . #\b)

       ("byte-2" . 98)

       ("number-6" . 35)

       ("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)