2020-11-02 22:11:45 +01:00
# goof-loop - a scheme looping facility
2021-05-18 18:12:01 +02:00
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.
2020-11-02 22:11:45 +01:00
Compared to foof-loop, some things are added. Apart from minor syntactic changes, subloops are supported. The best way is to show:
```
2020-11-09 13:35:43 +01:00
(define lst '((1 2) dud (3 4) (5 6)))
2020-11-25 20:40:48 +01:00
(loop ((:for a (in-list lst))
2021-02-18 21:19:12 +01:00
(:when (pair? a))
2020-11-25 20:40:48 +01:00
(:for b (in-list a))
(:acc acc (summing b)))
2020-11-02 22:11:45 +01:00
=> acc)
```
2020-11-25 20:40:48 +01:00
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.
2020-11-04 23:15:32 +01:00
2021-05-11 13:34:13 +02:00
## Beta warning
This is beta quality software, and some minor details are likely to change. I have gotten most kinks worked out though.
2021-03-07 22:26:15 +01:00
## Documentation
2021-05-11 13:27:09 +02:00
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.
2021-03-07 22:26:15 +01:00
2020-11-02 22:11:45 +01:00
## Differences from foof-loop
2021-05-18 18:12:01 +02:00
### lexical
foof-loop has a lot of code movement going on, and it can be hard to understand exactly where things end up. goof employs a more strict lexical hierarchy. The following is not possible in (chibi loop):
d into only after the above clauses have been evaluated.
2020-11-02 22:11:45 +01:00
### syntactical
2020-12-16 19:54:55 +01:00
for-clauses are split into :for and :acc clauses. This is because the addition of subloops means we have to treat accumulators differently.
2020-11-02 22:11:45 +01:00
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)
2021-01-28 20:18:15 +01:00
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.
2020-11-02 22:11:45 +01:00
2021-05-11 13:27:09 +02:00
### 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.
2020-11-04 23:15:32 +01:00
### Regressions compared to foof-loop
2020-11-02 22:11:45 +01:00
2021-01-28 20:18:15 +01:00
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).
2020-11-02 22:11:45 +01:00
2020-11-04 23:15:32 +01:00
Due to clause reordering, positional updates are not supported. If you want to update your loop vars, do so using named update (see below).
2020-11-02 22:11:45 +01:00
### changes
2021-01-28 20:18:15 +01:00
(with var [init [step [guard]]]) => (:for var (in init [step [stop-expr]])).
guard was a procedure, but now it is an expression.
2020-11-02 22:11:45 +01:00
2021-01-28 20:18:15 +01:00
(with var 10 (- var 1) negative?) => (:for var (in 10 (- var 10) (negative? var)))
2021-05-18 18:12:01 +02:00
## Features
2020-11-02 22:11:45 +01:00
2021-05-18 18:12:01 +02:00
### Lexical order of clauses
(loop ((:for a (in-list 1 2 3)
(:bind b (expensive-operation1 a))
(:when (test? b))
(:bind c (expensive-operation2 b))
(:when test2? c)
(:acc acc (listing c))))
=> acc)
### Loop naming to make it "fold right"
2020-11-02 22:11:45 +01:00
2021-05-11 13:27:09 +02:00
You can of course still have a larger control of when to loop by naming your loop:
2020-11-02 22:11:45 +01:00
```
2020-11-25 20:40:48 +01:00
(loop loopy-loop ((:for a (up-from 1 (to 11))))
2020-11-02 22:11:45 +01:00
=> '()
(if (odd? a)
(cons (* a (- a)) (loopy-loop))
(cons (* a a) (loopy-loop))))
;; => (-1 4 -9 16 -25 36 -49 64 -81 100)
```
2021-05-18 18:12:01 +02:00
### Named updates
2020-11-02 22:20:52 +01:00
```
;; Shamelessly stolen from Taylor Campbell's foof-loop documentation
2020-11-04 11:58:58 +01:00
(define (partition list predicate)
2020-11-25 20:40:48 +01:00
(loop continue ((:for element (in-list list))
(:acc satisfied (folding '()))
(:acc unsatisfied (folding '())))
2020-11-04 11:58:58 +01:00
=> (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))
2020-11-02 22:20:52 +01:00
```
2020-11-02 22:11:45 +01:00
2021-05-18 18:12:01 +02:00
### Exposing loop variables
The iterator protocol allows exposing the loop variables
```
(loop name ((:for elt pair (in-list '(1 2 3))))
=> '()
(if (null? (cdr pair))
(list elt)
(cons* elt ': (name))))
;; => (1 : 2 : 3)
```
### :final is context sensitive (compared to Racket's #:final)
``` scheme
(loop ((:for elt (in-list '( 1 2 3)))
:final (= elt 2)
(:for ab (in-list '(a b)))
(:acc acc (listing (cons elt ab)))
=> acc))
;; => ((1 . a) (1 . b) (2 . a) (2 . b))
```
The racket counterpart would result in ((1 . a) (1 . b) (2 . a))
### for-clauses can refer to eachother
The iterative fibonacci loop is weird to write using for/fold. goof fixes this:
``` scheme
(loop ((:for a (in 0 b))
(:for b (in 1 (+ a b)))
(:for count (up-from 0 (to 100)))
(:acc acc (listing b)))
=> acc
(display b) (newline))
```
### Accumulators and arbitrary code can be placed in subloops
``` scheme
(loop ((:for a (in-list '(1 2 3)))
(:acc aa (summing a))
(:do (display "Entering subloop!") (newline))
:subloop
(:for b (up-from a (:to (+ a 2))))
(:acc ab (listing b)))
=> (values aa ab))
;; => 6 (1 2 2 3 3 4)
```
2020-12-16 19:54:55 +01:00
### 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
2021-02-18 21:19:12 +01:00
(loop/first ((a (in-list '(a b c 3 4 d))) (:when (integer? a)))
2020-12-16 19:54:55 +01:00
(display a)
a)
;; => displays 3 and returns 3.
2021-02-18 21:19:12 +01:00
(loop/last ((a (in-list '(a b c d e f))) (:break (eq? a 'e)))
2020-12-16 19:54:55 +01:00
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
```
2021-05-18 18:12:01 +02:00
## Speed
2020-12-16 19:54:55 +01:00
2021-05-18 18:12:01 +02:00
Speed is good. Despite the rather involved expansion you can see in the documentation, due to inlining and dead-code elimination, the actual expansion shows some good code:
2020-12-16 19:54:55 +01:00
```
> ,opt (loop ((:for a (in-list '(1 2 3 4)))
2021-02-18 21:19:12 +01:00
(:when (even? a))
2020-12-16 19:54:55 +01:00
(: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)))
2021-05-11 13:27:09 +02:00
;; loop/list, being less general, produces faster code that can be more easily optimized
2020-12-16 19:54:55 +01:00
> ,opt (loop/list ((a (in-list '(1 2 3 4)))
2021-02-18 21:19:12 +01:00
(:when (even? a)))
2020-12-16 19:54:55 +01:00
a)
$2 = (list 2 4)
2021-05-11 13:27:09 +02:00
;; Removing the opportunity to completely optimize the loop away
2020-12-16 19:54:55 +01:00
> ,opt (loop/list ((a (in-list (read)))
2021-02-18 21:19:12 +01:00
(:when (even? a)))
2020-12-16 19:54:55 +01:00
a)
2021-05-11 13:27:09 +02:00
;; This is actually the preferred way to do it in guile. Guile re-sizes the stack, so no stack overflows
2020-12-16 19:54:55 +01:00
$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)))))
```
2020-11-02 22:11:45 +01:00
## Todo
2021-01-28 20:18:15 +01:00
Tests!
2020-11-04 11:58:58 +01:00
2021-01-28 20:18:15 +01:00
Finish documentation.
2020-11-02 22:11:45 +01:00
## 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
2021-05-11 13:27:09 +02:00
The same BSD-styled license Alex uses for chibi-loop.