
I have changed my mind about the clause forms. They should, with the exception of :subloop, be parethesised: :when test => (:when test).
257 lines
No EOL
11 KiB
Markdown
257 lines
No EOL
11 KiB
Markdown
# 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. |