Goof-loop

Linus Björnstam

Preface

We have all had the thought. Why oh why are not all our problems solved by a nice recursive algorithm over a list?. We see a neat problem. We smile and think about all the nice things we shall do right after finishing off this little tidbit, this delicious little bite of a problem. Then we scrape the surface. Under there is a jungle of state. Suddenly our little recursive list-mangling is not enough. To defeat the anxiety rising within us, we quickly write something using map, filter, zip and reduce. We test it. We smile again. We test it on real world data. The smile stiffens. The little bite has turned in to an o(n²) turd.

We raise our gaze and see the disgustingly mutating little wonders we could have achieved using python list comprehensions. We look sideways and see the magnificent, fantastic, but abominable loop macro in Common Lisp. Even they, the elitist ultra-productive scumbags, hate it, yet they sieze every moment to make underhanded comments about named lets. Why, oh why is the world not as elegant as we want it to be?

We think again about how python programmers write scheme, and shudder. No! Never that!, we think to ourselves. A vague idea starts forming. A quote surfaces from the depths of our minds “Above all the wonders of Lisp’s pantheon stand its metalinguistic tools; by their grace have Lisp’s acolytes been liberated from the rigid ascetism of lesser faiths”. Maybe we should try to do it ourselves. Thinking about it, we have actually written some macros before. Anaphoric ifs, some boilerplate removal macros, and oh! Even an almost-working implementation of SRFI-26. It passed nearly all the tests, and we couldn’t be bothered to go any further. But a looping facility… How hard can it be?

– Linus Björnstam, September 2020

Introduction

Goof-loop is like the name suggests a looping facility. It is based in many ways on things I learned porting Racket’s for-loops to guile and on the marvel that is (chibi loop) in chibi-scheme. The goals of goof-loop are:

The only other lisp looping facility that I know of that provides these things is Common Lisps iterate package. Iterate does however do a lot of things that are cumbersome to do in portable scheme, and would be prohibitively complicated to implement efficiently in a portable way. Unless, of course, one considers CPS-conversion of arbitrary scheme code using syntax-rules simple

On a side note: if anyone has the source of Olin Shivers’ loop package described in his paper “Anatomy of a loop”, please send me an email.

An example or two

So, how does it look? A slightly contrived example, a naive sieve of Erathostenes:

(define (erathostenes n)
  (define vec (make-vector n #t))
  (loop/list ((:for i (up-from 2 (:to n)))
              (:when (vector-ref vec i)))
    ;; Here we set all multiples of i to #f
    (loop ((:for j (up-from (* 2 i) (:to n) (:by i)))
      (vector-set! vec j #f))
    i))
        

Calling (erathostenes 10) returns a list of all primes below 10.

The example above can also be written using “subloops”:

(define (erathostenes n)
  (define vec (make-vector n #t))
  (loop ((:for i (up-from 2 (:to n)))
         (:when (vector-ref vec i))
         (:acc lst (listing i))
         (:for j (up-from (* 2 i) (:to n) (:by i))))
    (vector-set! vec j #f)))
        

Specification

The loop grammar is the following:

(loop [name] (loop-clause ...) [=> final-expr] body ...)

         name = identifier

  loop-clause = (:for id id* ... seq-expr)
              | (:acc id id* ... seq-expr)
              | (:bind (id id* ... expr) ...)
              | (:when guard-expr)
              | (:unless guard-expr)
              | (:break break-expr)
              | (:final guard-expr)
              | (:do expr ...)
              | :subloop

    seq-expr = a macro that conforms to the looping protocol described below.
  final-expr = The expression executed at loop finalization. If none is given,
               one returning all :acc values is generated
         body = A sequence of expressions executed after the loop clauses
                mostly useful to control in which fashion subsequent loops execute.
    

If a name is provided, it will be bound to a macro that allows named update of loop variables:

(loop lp ((:for a (in 0 (+ a 1)))
          (:break (> a 9)))
  => '()
  (if (= 4 a)
      (cons a (lp (=> a 8)))
      (cons a (lp))))
    

This rather inane example would return (0 1 2 3 4 8 9). Read more about this in the part about loop variables.

Subloops

A subloop is a distinction between an outer loop and an inner loop. A subloop means that: for each element yielded by an outer loop, the inner loop is run until exhaustion. If a :for clause is placed after any non-:for clause, it is considered a subloop.

(loop ((:for a (in-list '(1 2 3))) 
        :subloop
        (:for b (up-from 0 (:to a)))
        (:acc acc (listing (cons a b)))))
  
;; => ((1 . 0) (2 . 0) (2 . 1) (3 . 0) (3 . 1) (3 . 2))
      

The above :subloop clause is equivalent to :when #t and :unless #f. A :break clause will immediately stop execution of the loop:

(loop ((:for a (in-list '(1 2 3))) 
       (:break (= 3 a))
       (:for b (up-from 0 (:to a)))
       (:acc acc (listing (cons a b)))))
  
;; => ((1 . 0) (2 . 0) (2 . 1))
      

And a :final guard will let the subsequent subloops execute once.

(loop ((:for a (in-list '(1 2 3 4)))
       (:final (= 3 a))
       (:for b (up-from 0 (:to a)))
       (:acc acc (listing (cons a b)))))
  
;; => ((1 . 0) (2 . 0) (2 . 1) (3 . 0) (3 . 1) (3 . 2))
      

The :final clause is actually equivalent to something alike the following:

(loop ((:for a (in-list '(1 2 3 4)))
       ;; this works because :acc bindings are promoted "outwards".
       (:break final)
       (:acc final (in-value (initial #f) #t (if (= 3 a))))
       (:acc acc (listing (cons a b)))))
      

This means that any clause above the final binding will be executed an extra time before the loop exits:

(loop ((:for a (up-from 1 4))
       (:acc lst (listing a))
       (:final (= a 2))
       (:acc lst2 (listing a))))
;; => (1 2 3) (1 2)
        

Loop variables

Both accumulating clauses and :for clauses have something called loop variables. In the case of (:for elt (in-list lst)) the loop variable would be the current pair where elt is the car. Some :acc- or :for-clauses may expose their loop variables so that they can be queried or even updated.

In the case of the menioned in-list we can choo se to expose the name of the current pair, as in the following example:

(define (interpose lst between)
  (loop lp ((:for elt pair (in-list lst)))
    => '() ;; reached only if lst is empty
    (if (null? (cdr pair))
        (list elt)
        (cons* elt between (lp)))))
        
(interpose '(1 2 3 4) ':)        
; => (1 : 2 : 3 : 4)        
      

In the above example we chose to bind the loop variable of in-list to pair. Using pair we can then query if the next iteration is the empty list and decide not to interpose any value.

Some loop clauses have the option of exposing their loop variable(s). The option to do so is documented under the documentation for each clause.

Simple forms

The pure loop macro is quite a big hammer for most tasks. Often we want to do simple things, like collect elements into a list or a vector, which means the extra housekeeping of separating accumulators and for clauses are too much heavy lifting. goof-loop provides several simpler forms that can be used in those cases. In these simpler forms :acc is disallowed, and everything not identified as anything else is assumed to be a :for clause. The loop below accumulates the 100 first fibonacci numbers into a list.

(loop/list ((count (up-from 0 (:to 100)))
            (a (in 0 b))
            (b (in 1 (+ a b))))
  b)
      

The simple forms provided by goof-loop are the following:

Scheme syntax: loop/first
(loop/first [:default #f] (clauses ...) body ...)

If any body is ever evaluated, stop and return the value of that evaluation. If no body is evaluated it returns the value specified by :default, which defaults to #f.

Scheme syntax: loop/last
(loop/last [:default #f] (clauses ...) body ...)

Returns the result of the last body to be evaluated. If no body is evaluated it returns the value specified by :default, which defaults to #f.

Scheme syntax: loop/list
(loop/list (clauses ...) body ...)

Iterates over clauses and builds a list of the result of every evaluation of body. The order of the list is the same as the order body was evaluated in. The result of the first evaluation of body is the first element of the resulting list.

The list returned is the same even when used with multi-shot continuations.

If no body is evaluated, the result is the empty list.

Scheme syntax: loop/product
(loop/product (clauses ...) body ...)

Multiplies each evaluated body. If no body is evaluated, 1 is returned

Scheme syntax: loop/sum
(loop/sum (clauses ...) body ...)

Sums the result of each evaluated body. If no body is evaluated, 0 is returned.

Scheme syntax: loop/and
(loop/and (clauses ...) body ...)

If all evaluated bodies return truthy, return the result of the last evaluated body. If any body returns #f, stop iteration and return #f. If no body is evaluated, #t is returned.

Scheme syntax: loop/or
(loop/or (clauses ...) body ...)

If any evaluated body returns truthy, stop iteration and return the result of that body. If no body returns truthy, return #f. If no body is evaluated, return #f.

Scheme syntax: loop/list/parallel
(loop/list/parallel (clauses ...) body ...)

Like loop/list, but evaluates each body in parallel.

:for-clauses

A :for clause specifies a number of identifiers that are bound to a value from a sequence. (:for i (up-from 0 10))) will bind a to all integers between 0 and 10 (excluseve) in order. The first time the loop is executed, it is bound to 0, then 1, then 2, and so on. Some loops has support for exposing their loop variables to the user. In the case of (:for elt pair (in-list lst)), elt will be bound to the car of pair, whereas pair will be bound to the loop variable used by in-list to hold the current position of the list.

Scheme syntax: in
(:for binding (in start [update [stop]]))

Binds a loop variable to binding. It’s first value is start. It is updated by the update expression, or is left unchanged if no such expression is present. If a stop expression is provided, it will be evaluated before each loop body. If the stop expression returns true, the iteration will be considered exhausted.

(loop ((:for a (in 0 b)) (:for b (in 1 (+ a b) (> b 20))))
  (display b) (newline))
        

Will print all fibonacci numbers below 20.

binding is a loop variable and can thus be changed using named updates.

Scheme syntax: up-from
(:for binding (up-from start [(:to bound)] [(:by step)]))
(:for binding (up-from start [bound [by]]))

Binds binding to the number start up to bound (exclusive!) by step. If no bound is given, it will yield values indefinitely. The second shorter form will not allow unbounded iteration if a step other than 1 is wanted.

binding is a loop variable and can thus be changed using named updates.

Scheme syntax: down-from
(:for binding (down-from start [(:to bound)] [(:by step)])
(:for binding (down-from start [bound [by]]))

Binds binding to the number (- start 1) down to bound (inclusive!) by step. If no bound is given, it will yield values indefinitely. The second shorter form will not allow unbounded iteration if a step other than 1 is wanted.

binding is a loop variable and can thus be changed using named updates.

Scheme syntax: in-list
(:for binding [pair] (in-list expr [by])

Binds binding to the car of the loop variable pair. pair is advanced by applying the procedure by to it (defaulting to cdr). The iteration stops when pair is the empty list.

Scheme syntax: in-lists
(:for binding [pairs] (in-lists expr [by])

Works the same as in-list, but expr must evaluate to a list of lists. binding is bound to the car of those lists, and they are advanced by by, defaulting to cdr.

Scheme syntax: in-vector
(:for binding [index] (in-vector expr [low [high]]))

Binds binding to all elements in the vector produced by expr in order from low to high. low defaults to 0 and high defaults to the last index of the vector.

The vector produced by expr is bound outside the loop body, and index is the loop variable.

Scheme syntax: in-reverse-vector
(:for binding [index] (in-reverse-vector expr [high [low]]))

Binds binding to all elements in the vector produced by expr in reverse order from high to low. high defaults to the last element of the vector and low defaults to 0.

The vector produced by expr is bound outside the loop body, and index is the loop variable.

Scheme syntax: in-string
(:for binding [index] (in-string expr [low [high]]))

Binds binding to all elements in the string produced by expr in order from low to high. low defaults to 0 and high defaults to the last index of the string.

The string produced by expr is bound outside the loop body, and index is the loop variable.

Scheme syntax: in-reverse-string
(:for binding [index] (in-reverse-string expr [high [low]]))

Binds binding to all elements in the vector produced by expr in reverse order from high to low. high defaults to the last element of the vector and low defaults to 0.

The string produced by expr is bound outside the loop body, and index is the loop variable.

Scheme syntax: in-port
(:for binding (in-port port [reader [eof?]]))

Binds binding to the result of calling reader on port. Iteration stops when (eof? binding) returns true.

Scheme syntax: in-file
(:for binding (in-file path [reader [eof?]]))

Opens the file located at path (which is a string) and binds binding to the result of calling reader on the opened port. Iteration stops when (eof? binding) returns true.

Scheme syntax: in-generator
(:for binding (in-generator gen))

Binds binding to the result of calling the SRFI-158-compatible generator gen. Iteration stops when gen returns the end-of-file object.

Scheme syntax: in-hash
(:for binding (in-hash hash))

Binds binding to the (key . value) pairs of the hash-table hash. May, as all body-binding variables, be pattern-matched:

(loop/list (((_ . val) (in-hash hash-table)))
  val)
        
Scheme syntax: in-cycle
(:for binding (in-cycle iterator))

Binds the values produced by iterator to binding. Once iterator stops, it starts over.

Scheme syntax: in-indexed
(:for binding (in-indexed iterator))

Binds binding to (n . value), where n is the nth value produced by iterator and value is that value. n starts from 0

Scheme syntax: stop-before
(:for binding (stop-before iterator pred))

Binds binding to the values produced by iterator until pred applied to that value returns true. The iterator is then considered exhausted. Useful in subloops where one might want to end internal iteration without :break-ing.

Scheme syntax: stop-after
(:for binding (stop-after iterator pred))

Binds binding to the values produced by iterator until pred applied to that value returns true. It then produces that last value. The iterator is then considered exhausted. Useful in subloops where one might want to end internal iteration without :break-ing.

:acc-clauses

Accumulating clauses differ from :for-clauses in 2 significant ways. They have a final value available in the final-expr, and they keep their state throughout the loop. In the case of a loop with one subloop, the :for-clauses reset their state every time the subloop is entered. :acc-clauses will always keep their state.

Another small thing is that for some :acc-clauses, the binding may sometimes only be visible to the user in the final-expr, but like :for-clauses they sometimes offer the programmer to name the loop variables.

Many accumulating clauses support an if form. If such a clause is given, accumulation will only happen if the guard clause returns true.

Scheme syntax: listing
(:acc binding (listing [(:initial init)] expr [if guard]))

Accumulates expr into a list. ´bindingis only accesible in the final-expression. The list is in the same order as the loop bodies were evaluated. Ifinitialis given that will be used as the tail of the accumulated results. It defaults to’()`.

Scheme syntax: listing-reverse
(:acc binding (listing-reverse [(:initial init)] expr [if guard]))

The same as listing but the resulting list in in reverse order. If the order of the resulting list does not matter, this will be faster than the regular listing as it will not preform any reverse at the end.

Scheme syntax: appending
(:acc binding (appending [(:initial init)] expr [if guard]))

expr evaluates to a list that is then appended to the accumulated result.

(loop ((:for elt (in-list '((1 2) (3 4))))
       (:acc acc (appending (:initial '(0)) elt)))
  => acc)
  ;; => (0 1 2 3 4)     
        
Scheme syntax: appending-reverse
(:acc binding (appending-reverse [(:initial init)] expr [if guard]))

expr evaluates to a list that is then consed element by element onto the already accumulated results. The default initial value is '().

(loop ((:for elt (in-list '((1 2) (3 4))))
       (:acc acc (appending-reverse (:initial '(0)) elt)))
  => acc)
  ;; => (4 3 2 1 0)
        
Scheme syntax: summing
(:acc binding (summing [(:initial init)] expr [(if guard)]))

Adds the result of expr together using +. The default initial value is 0.

Scheme syntax: multiplying
(:acc binding (multiplying [(:initial init)] expr [(if guard)]))

Multiplies the result of expr using *. The default initial value is 1.

Scheme syntax: hashing
(:acc binding (hashing [(:initial init)] key value [(if guard)]))

Adds the mapping (key => value) to the hashtable binding using equal?-hashing. The initial hash table is an empty hash-table. binding is bound to the hash table throughout the loop, and its content can be mutated in the loop body.

Scheme syntax: hashving
(:acc binding (hashving [(:initial init)] key value [(if guard)]))

Adds the mapping (key => value) to the hashtable binding using eqv?-hashing. The initial hash table is an empty hash-table. binding is bound to the hash table throughout the loop, and its content can be mutated in the loop body.

Scheme syntax: hashqing
(:acc binding (hashqing [(:initial init)] key value [(if guard)]))

Adds the mapping (key => value) to a hashtable using eq?-hashing. The initial hash table is an empty hash-table.binding is bound to the hash table throughout the loop, and its can be mutated in the loop body.

Scheme syntax: vectoring
(:acc binding [index] (vectoring expr [(:length len) [(:fill fill)]]))

Accumulates the result of expr into a vector. If len and fill is given the vector will be at most len elements long and any unfilled indexes will contain the element fill. The loop will exit when len elements have been accumulated.

If length is not given, the vector will be expanded as required.

A vectoring clause adds an implicit (:break (= index len)) after the vectoring clause. Once the last element of the vector is filled, the loop will stop and no subsequent clauses or body will be executed.

binding is bound to the vector throughout the loop, and its content mutated in the loop body.

Loop protocol

goof-loop is extensible using regular syntax-rules macros. The protocol for both :acc- and :for-clauses is identical, except that the behaviour of the different parts are slightly different.

:for-clauses

The following example defines the simple :for-driver in-alist:

(define-syntax in-alist
  (syntax-rules ()
    ((_ ((key val) (expr)) next . rest)
      (next
        ;; Outer let bindings. These are bound before the loop body
        ;; and are in the form ((id id* ... expr) ...).
        ()
        ;; Loop variables. Here we bind %cursor to expr, which at each iteration is updated
        ;; by (cdr %cursor). In the form ((name init update) ...)
        ((%cursor expr (cdr %cursor)))
        ;; stop guards. In the form (stop-expr ...)
        ((null? %cursor))
        ;; Body bindings. If we need to do anything to the loop variables before
        ;; any tests except the stop guards are run, this is where we do it.
        ;; In the form ((bindings ... expr) ...) where the bindings may be
        ;; (ice-9 match) patterns.
        (((key . val) (car %cursor)))
        ;; This last one is finalizers. They run on any kind of exit from the same
        ;; loop or any subloop where the :for-clause is in scope.
        () . rest))))
(register-loop-clause 'for #'in-alist)        
      

In short, the clause (:for key value (in-alist alist-expr)) expands to:

(in-alist ((key val) (alist-expr)) next-macro . rest)
      

You should never have to care about rest. That is the complete state of the expansion of loop, which we have to pass around since most of goof-loop is written in syntax-rules.

Going from the top we first have the outer let bindings. These are bound outside the loop, and are mostly used for binding things like vectors or ports that do not change during the loop.

The next one are loop variables. Here we provide three things: variable name, initial expression and update. The update expression is used to bind the variable to the next value of the sequence.

Stop guards are just that. In this case, when (null? %cursor)) returns true, the sequence is considered exhausted. If the loop is in a subloop, the current loop stops and the outer loop continues. If there is only one loop, it halts.

Body bindings are bound before anything except the stop guards are run. They may be an (ice-9 match) pattern, and are multiple-value aware. (a b (div-and-mod 10 7)) is valid, and so is ((key . val) (let ((c (car %cursor))) (values (car c) (cdr c)))).

Finalizers are run everytime the current loop or any subloop below the current loop exits. This can be used to close resources. (in-file ...) uses this.

register-loop-clause registers the loop clause so that it can be verified to exist at expansion time.

:acc-clauses

The following code implements the accumulator (alisting …) which accumulates two values into an alist:

(define-syntax alisting
  (syntax-rules (:acc)
    ((_ :acc ((name) (key val)) next . rest)
      (next
        ;; Outer let bindings. These are bound before the loop body
        ;; and are in the form ((id id* ... expr) ...).
        ()
        ;; Loop variables. Here we bind %cursor to expr, which at each iteration is updated
        ;; by (cdr %cursor). In the form ((name init update) ...)
        ((%cursor '() (cons (cons key val) %cursor)))
        ;; stop guards. Currently do nothing.
        ()
        ;; Body bindings. Not used much.
        ()
        ;; final-bindings. The same thing as wrapping the final-expr in
        ;; (let ((name (reverse %cursor))) ...)
        ((name (reverse %cursor)))
        . rest))))

        (register-loop-clause 'acc #'alisting)
      

The first difference is that the first argument to an accumulator always is the symbol :acc.

So, number one is the same as for :for-clauses. Number two looks the same, except the name %cursor and it’s initial value is promoted to the outermost loops. This is because the value of the accumulator needs to be available in all stages of the loop to be available in the final-expr. The initial value cannot therefore reference any things bound in outer loops.

No loop currently use the stop guards, this is because the stop guards do not stop iteration completely, just end the current loop. This might change.

The body bindings can be used to bind variables to make information from the accumulator visible, but otherwise not used.

Final bindings. Binds whatever variable name you chose to whatever expression you chose in the final-expression.

register-loop-clause registers the alisting clause so that it can be verified to exist at expansion time.

Loop expansion

The main chunk of a loop expands into something like the following: A goof loop expands into something looking like this:

(let* (OUTER-BINDING ...
       final-function (lambda (FINAL-BINDING) FINAL-EXPR))
  (let goof-loop ((ACCUMULATOR ACCUMULATOR-INIT) ... (LOOP-VAR LOOP-VAR-INIT) ...)
    (if (or CHECK...)
      (begin
        FOR-CLAUSE-FINALIZER ...
        (final-function (ACCUMULATOR-FINALIZER ACCUMULATOR) ...))
      (let ((BODY-BINDING ... BODY-BINDING-EXPR) ...)
        (match-let ((PARENTHESISED-PATTERN MATCH-EXPR))
          (CLAUSES-EXPANSION
            LOOP-BODY ...))))))

;; CLAUSES-EXPANSION:
;; ((:when test). rest) =>
(if test
    (begin . rest)
    (goof-loop ACCUMULATOR ... LOOP-VAR-NEXT ...))

;; ((:unless test) . rest)
;; is the same as above, but the test is negated

;; ((:acc var (accumulate-expr bleh)) . rest)
(let ((ACCUMULATOR ACCUMULATE) ...)
  (if (or ACCUMULATOR-TESTS ...)
      (begin FOR-CLAUSE-FINALIZER ...
             (final-function (ACCUMULATOR-FINALIZER ACCUMULATOR) ...))
      (begin . rest)))

;; ((:break test) . rest)
(if test
    (begin FOR-CLAUSE-FINALIZER ...
           (final-function (ACCUMULATOR-FINALIZER ACCUMULATOR) ...))
    (begin . rest))

;; ((:bind (USER-BINDING ... expr) ...) . rest)
(let ((USER-BINDING ... expr) ...)
  (match-let ((PARENTHESISED-PATTERN MATCH-EXPR) ...)
    . rest))

;; ((:do expr ...) . rest)
(begin expr ...)
. rest

;; ((:final test) . rest)
((:break final)
 (:acc final (special-final-accumulator (initial #f) #t (if test)))
 . rest)

    

OUTER-BINDING: are provided by accumulators or for clauses for bindings that are not passed as an argument to the loop, for example a vector. The vector is bound here, and the index into the vector is the thing iterated over.

FINAL-BINDING and FINAL-EXPR: When the iteration ends, this function is called with the results of the :acc clauses. In the case of (:acc lst-acc (listing …)), the name of the accumulator is never lst-acc in the loop body, but only in the FINAL-EXPR. In case of (listing …) the accumulated results are reversed before the final function.

ACCUMULATOR and LOOP-VAR: ACCUMULATOR holds the current state of an accumulator clause. This is not necessarily the same binding as the user provided as the name, as described above. LOOP-VAR is the current state of a :for clause.

CHECK: Checks for :for-clauses. In the case of (in-list …) this would check for (not (pair? …)).

FOR-CLAUSE-FINALIZER: some :for clauses need to be finalized. In the case of (in-file …) the open file handle is closed at any point where the iteration stops.

ACCUMULATOR-FINALIZER: ACCUMULATOR-FINALIZER is any preprocessing done to ACCUMULATOR before passing it on to the final-function. In the case of (listing …) that would be (reverse …).

BODY-BINDING and BODY-BINDING-EXPR: BODY-BINDING are the names the user provided for the body bindings. In the case of (:for a (in-list ’(1 2 3))) the body binding would be (a (car name-of-loop-variable)). The body binding may be an (ice-9 match) pattern. More on that below.

PARENTHESISED-PATTERN and MATCH-EXPR: If a USER-BINDING or BODY-BINDING is not an identifier, it is presumed to be a match-let pattern. The result is bound to a variable and matched against this match-let.

LOOP-BODY, ACCUMULATE, and LOOP-VAR-NEXT: The user supplied body of the loop. If the loop is not named (i.e: in loops where the user controls the iteration) an expression for the next loop iteration is added to the body. ACCUMULATE is the expression the accumulator clause provided to accumulate a new value. For (:acc acc (listing elem)) that is (cons elem acc). LOOP-VAR-NEXT is the expression evaluated to get the next iteration’s loop variable. In the case of (in-list lst) that is (cdr lst). If a loop name is provided there is no implicit next loop.

ACCUMULATOR-INIT and LOOP-VAR-INIT: ACCUMULATOR-INIT are ALL accumulator init values, including the ones in subloops. For (listing …) that is the empty list. LOOP-VAR-INIT is the initial loop vars.

USER-BINDING: an identifier or an (ice-9 match) pattern. If any of the supplied USER-BINDINGs are patterns, they are destructured in the subsequent match-let. goof uses let and let* from srfi-71, and as such is multiple-values-aware. You can do (:bind (one (fst . snd) (values 1 (cons 3 4)))), and it will work as expected.

In case of subloops, those are placed instead of LOOP-BODY. They use the same final-function, and instead of quitting when any CHECK triggers they go out to the outer loop.

Porting

The bulk of goof-loop is written in portable syntax-rules. That code can be found in goof-impl.scm and all files under the goof directory. The major non-portable part is the macro that is bound in every loop to the user-given name of the loop. In the guile implementation this is implemented in syntax-case, and should be portable to any r6rs scheme. The guile implementation does a non-hygienic comparison of the variables in the named update, so to not have to deal with unwanted shadowing:

(loop lp ((:for a (up-from 0 10)))
  => '()
  (let ((a (+ a 1)))
    (if (odd? a)
        (cons a (lp (=> a (+ a 2))))
        (lp))))
;; => (1 5 9)        
    

This is just a matter of comfort, since this kind of DWIM has no negative impact. I would regard an implementation without this as conforming as well.

The other part is loop/list/parallel. This is not required to actually be parallel, so defining it as an alias to loop/list is acceptable.

The third part is also a comfort macro; valid-clause?. It simply verifies that the loop clause is bound and registered as a loop clasue. It makes it possible to get good error messages when a loop clause is misspelled or not imported. It can be portably defined as:

(define-syntax valid-clause?
  (syntax-rules ()
    ((_ rest ... ) (rest ...))))
    

at the expense of error messages that become, for lack of a better word, ultra-shit. The register-loop-clause form can be implemented as whatever NOP you prefer if you chose the above definition. If not, it takes two arguments, type (a symbol of either ’for or ’acc) and the appropriate object. In the guile version this is a syntax object, which is used because it allows the user to import the clause under a different name.

If your scheme of choice lacks these kinds of macrobatics, a simple symbol list helps a little bit, and is regarded as conformant.