A looping facility for guile.
Find a file
Linus 1cd5e4edaa Added beta warning
I'm about to tag a release. Beta quality warning is probably an
understatement.
2021-05-11 13:34:13 +02:00
documentation Updated the documentation, added pointer to hosted version 2021-05-11 13:27:09 +02:00
goof Added stop-after and stop-before 2021-05-11 09:48:21 +02:00
example.scm Made it a module. 2020-12-16 20:17:13 +01:00
goof-impl.scm Added some tests. 2021-03-22 19:30:09 +01:00
goof.scm Fixed error reporting of missing :for clause 2021-05-11 10:00:46 +02:00
LICENCE Added a LICENCE file and fixed a small readme error. 2020-11-02 22:17:48 +01:00
README.md Added beta warning 2021-05-11 13:34:13 +02:00
tests.scm Added some tests. 2021-03-22 19:30:09 +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))

Beta warning

This is beta quality software, and some minor details are likely to change. I have gotten most kinks worked out though.

Documentation

The current WIP documentation can be found here: https://bjoli.srht.site/doc.html

It is written in a weird markdown/xml chimaera. You can find it in documentation doc.xml (for the weird format) and documentation/doc.html for the slightly more accessible HTML format.

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.

Higher order loop protocol

goof supports a higher order looping protocol, based on srfi-158 generators:

(loop ((:for food (in-list '(banana cake grape cake bean cake)))
       (:for true? (in-cycle (in-list '(#t #f)))))
   (display "The ") 
   (display food)
   (display " is a ")
   (if true?
      (display food)
      (display "LIE!"))
  (newline))

In the above example true? never ends, but restarts every time the list is exhausted.

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 when to loop by naming your loop:

(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  

Speed

Speed is good. Despite the rather involved expansion you can see in the documentation, 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 optimized
> ,opt (loop/list ((a (in-list '(1 2 3 4)))
                   (:when (even? a)))
         a)
$2 = (list 2 4)

;; Removing the opportunity to completely optimize the loop away
> ,opt (loop/list ((a (in-list (read)))
                   (:when (even? a)))
         a)

;; This is actually the preferred way to do it in guile. Guile re-sizes the stack, so no stack overflows
$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.