A looping facility for guile.
Find a file
Linus bb0de3e949 Added some generator clauses
Now in-file, in-port, in-lists and in-generator have generator clauses
2021-02-18 22:08:34 +01:00
documentation Added basic documentation 2021-01-28 13:30:32 +01:00
goof Added some generator clauses 2021-02-18 22:08:34 +01:00
example.scm Made it a module. 2020-12-16 20:17:13 +01:00
goof-impl.scm More parentheses 2021-02-18 21:19:12 +01:00
goof.scm Added some generator clauses 2021-02-18 22:08:34 +01:00
LICENCE Added a LICENCE file and fixed a small readme error. 2020-11-02 22:17:48 +01:00
README.md More parentheses 2021-02-18 21:19:12 +01:00
tests.scm Big, buggy commit 2020-11-25 20:40:48 +01:00

goof-loop - a scheme looping facility

goof-loops aims to be an amalgamation of the racket for loops and Alex Shinn's (chibi-loop). We are many that found racket's for loops a breeze of fresh air, but in the end their most general forms (for/fold and for/foldr) are kinda odd to work with. If you choose not to use those general for loops, you cannot express arbitrary transformations, like say a fibonacci sequence, since for clauses cannot reference eachother. goof-loop tries to fix this:

(loop ((:for a (in 0 b))
       (:for b (in 1 (+ a b)))
       (:for count (up-from 0 (to 1000)))
       (:acc acc (listing b)))
  => acc
  (display b) (newline))

The above example will display and accumulate the 1000 first fibonacci numbers. Doing the same thing in racket requires you to manually handle all the state in fold-variables using for/fold. It is a simple example, but proves the usefulness of goof-loop.

Compared to foof-loop, some things are added. Apart from minor syntactic changes, subloops are supported. The best way is to show:

(define lst '((1 2) dud (3 4) (5 6)))
(loop ((:for a (in-list lst))
       (:when (pair? a))
       (:for b (in-list a))
       (:acc acc (summing b)))
  => acc)

This will sum all the sublists of lst and produce the result 21. Any :when, :unless, :break, :final, or :subloop clause will break out a subloop if any subsequent for clauses are found.

Accumulators can be in any of the loop's stages:

(loop ((:for a (in-list '(1 2 3)))
       (:acc aa (summing a))
       :subloop
       (:for b (up-from a (to (+ a 2))))
       (:acc ab (listing b)))
  => (values aa ab))
;; => (values 6  (1 2 2 3 3 4))

Differences from foof-loop

syntactical

for-clauses are split into :for and :acc clauses. This is because the addition of subloops means we have to treat accumulators differently.

while and until are removed in favour of :break.

:when and :unless are added to better control when the loop body is executed (and accumulators accumulated)

with-clauses are removed in favour of (:for var (in init [step [stop]])) in case of loop clauses, or (:acc var (folding init [step])) in case of accumulators.

Regressions compared to foof-loop

only accumulating clauses are visible in the final-expression. This is due to sequence clauses not being promoted through to outer loops (since they should not keep their state if an inner loop is exited).

Due to clause reordering, positional updates are not supported. If you want to update your loop vars, do so using named update (see below).

changes

(with var [init [step [guard]]]) => (:for var (in init [step [stop-expr]])).

guard was a procedure, but now it is an expression.

(with var 10 (- var 1) negative?) => (:for var (in 10 (- var 10) (negative? var)))

similarities

You can of course still have a larger control of your loops:

(loop loopy-loop ((:for a (up-from 1 (to 11))))
  => '()
  (if (odd? a)
      (cons (* a (- a)) (loopy-loop))
      (cons (* a a) (loopy-loop))))

;; => (-1 4 -9 16 -25 36 -49 64 -81 100)

Named updates also work.

;; Shamelessly stolen from Taylor Campbell's foof-loop documentation
(define (partition list predicate)
  (loop continue ((:for element (in-list list))
                  (:acc satisfied (folding '()))
                  (:acc unsatisfied (folding '())))
     => (values (reverse satisfied)
                (reverse unsatisfied))
     (if (predicate element)
         (continue (=> satisfied (cons element satisfied)))
         (continue (=> unsatisfied (cons element unsatisfied))))))

(partition '(1 2 3 4 5) odd?)
;; => (values (1 3 5) (2 4))

Simple forms

I also provide simplified forms for many common operations. Omitting :for is allowed, and :acc clauses are not allowed.

(loop/list ((a (up-from 0 3)))
  a)
;; => (0 1 2)

(loop/sum ((:for a (up-from 1 4))) a)
;; => 6

(loop/product ((a (in-list '(2 3 4))))
  a)
;; => 24  

(loop/first ((a (in-list '(a b c 3 4 d))) (:when (integer? a)))
  (display a)
  a)
;; => displays 3 and returns 3.   

(loop/last ((a (in-list '(a b c d e f))) (:break (eq? a 'e)))
  a)
;; => 'd

(loop/and ((a (in-list '(1 2 3 'error))))
  (< a 3))
;; => #f

(loop/or ((a (in-list '(1 2 3 4))))
  (symbol? a))
;; => #f

(loop/list/parallel ((a (in-list '(42 41 43))))
  (expensive-function a))
;; => same result as loop/list, but faster if the problem parallelizes well  

Loop expansion

A goof loop expands into something looking like this:

(let* (<outer-let>)
  (letrec ((final-function (lambda (<final-binding>) <final-expr>))
           (goof-loop (lambda (<accumulator> ... <loop-var> ...)
                        (if (or <check> ...)
                            (begin
                              <for-clause-finalizer> ...
                              (final-function (<accumulator-finalizer> <accumulator>) ...))
                            (let ((<body-binding> ... <body-binding-expr>) ...)
                              (let ((<user-binding> ... <user-binding-expr>) ...)
                                (match-let ((<parenthesised-pattern> <match-expr>))
                                  (if (and <when-expr> ...)            
                                      (cond
                                        ((or <user-break> ...)
                                         <for-clause-finalizer> ...
                                         (final-function (<accumulator-finalizer> <accumulator>) ...))
                                        (else
                                          <loop-body>
                                          (goof-loop <accumulate> ... <loop-var-next> ...))
                                      (goof-loop <accumulator> ... <loop-var-next>  ...))))))))
    (goof-loop <accumulator-init> ... <loop-var-init> ...)))

<outer-let>: 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-variable>: <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> 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.

<when-expr>: the user supplied :when or :unless guard expression.

<user-break>: user-supplied :break guard.

<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.

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.

Speed

Speed is good. Despite the rather involved expansion above, due to dead-code elimination, the actual expansion shows some good code:

> ,opt (loop ((:for a (in-list '(1 2 3 4)))
              (:when (even? a))
              (:acc acc (listing a)))
         => acc)
$1 = (let loopy-loop ((cursor-1 '()) (cursor '(1 2 3 4)))
  (if (pair? cursor)
    (let ((a (car cursor)) (succ (cdr cursor)))
      (if (even? a)
        (loopy-loop (cons a cursor-1) succ)
        (loopy-loop cursor-1 succ)))
    (reverse cursor-1)))
    
;; loop/list, being less general, produces faster code that can be more easily unroled and optimized.    
> ,opt (loop/list ((a (in-list '(1 2 3 4)))
                   (:when (even? a)))
         a)
$2 = (list 2 4)

;; Removing the opportunity to completely remove the loop
> ,opt (loop/list ((a (in-list (read)))
                   (:when (even? a)))
         a)
         
$5 = (let loopy-loop ((cursor (read)))
  (if (pair? cursor)
    (let ((a (car cursor)) (succ (cdr cursor)))
      (if (even? a)
        (cons a (loopy-loop succ))
        (loopy-loop


;; The code expansion of the partition procedure above produces
(define (partition list predicate)
  (let loopy-loop ((satisfied '()) (unsatisfied '()) (cursor list))
    (if (pair? cursor)
      (let ((element (car cursor)) (succ (cdr cursor)))
        (if (predicate element)
          (loopy-loop (cons element satisfied)
                      unsatisfied
                      succ)
          (loopy-loop satisfied
                      (cons element unsatisfied)
                      succ)))
      (values (reverse satisfied) (reverse unsatisfied)))))


Todo

Tests!

Finish documentation.

add generator support for all provided iterators

foof, what a guy

I have previously expressed some admiration for Alex and I will do it again. The source of chibi loop is extremely elegant, and all but the hairiest part is written in syntax-rules. Not only has he written my two favourite SRFIs, his input in all the other discussions I have seen is always on-point, pragmatic and generally fantastic. He neither knows of this project, nor embraces it in any way. Y'all should go look at the source of (chibi loop) though.

Licence

The same BSD-styled license Alex uses for chibi-loop.