|
||
---|---|---|
documentation | ||
goof | ||
example.scm | ||
goof-impl.scm | ||
goof.scm | ||
LICENCE | ||
README.md | ||
tests.scm |
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.
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.
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
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.
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)))
Features
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"
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
;; 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))
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)
(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:
(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
(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)
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 inlining and 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.
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.