Big change: lexical scoping

This introduces lexical scoping of for clauses. See README.md
This commit is contained in:
Linus 2021-05-18 18:12:01 +02:00
parent 769553832b
commit 2c323be362
5 changed files with 132 additions and 63 deletions

107
README.md
View file

@ -1,17 +1,6 @@
# 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.
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:
@ -26,18 +15,6 @@ Compared to foof-loop, some things are added. Apart from minor syntactic changes
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.
@ -51,6 +28,11 @@ It is written in a weird markdown/xml chimaera. You can find it in documentation
## 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.
@ -90,8 +72,20 @@ Due to clause reordering, positional updates are not supported. If you want to u
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
### similarities
### 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:
@ -105,7 +99,7 @@ You can of course still have a larger control of when to loop by naming your loo
;; => (-1 4 -9 16 -25 36 -49 64 -81 100)
```
Named updates also work.
### Named updates
```
;; Shamelessly stolen from Taylor Campbell's foof-loop documentation
@ -123,6 +117,61 @@ Named updates also work.
;; => (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)
``` 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)
```
### Simple forms
I also provide simplified forms for many common operations. Omitting :for is allowed, and :acc clauses are not allowed.
@ -161,9 +210,9 @@ I also provide simplified forms for many common operations. Omitting :for is all
```
### Speed
## 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:
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)))
@ -220,8 +269,6 @@ 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.

View file

@ -29,7 +29,7 @@
(define-aux-syntaxes
;; Auxiliary syntax for the loop clauses
:when :unless :break :final :bind :subloop :for :acc
:when :unless :break :final :bind :do :subloop :for :acc
;; Auxiliary syntax for the iterators.
:gen
;; auxiliary auxiliary syntax
@ -121,7 +121,7 @@
;; cl sorts all the clauses into subloops and positions everything where it should be.
(define-syntax cl
(syntax-rules (=> :for :acc :when :unless :break :final :bind :subloop)
(syntax-rules (=> :for :acc :when :unless :break :final :do :bind :subloop)
((_ orig name l a v c r f ff user () => expr . body)
(emit orig name l a v c r f ff user expr . body))
((_ orig name l a v c r f ff user () . body)
@ -143,8 +143,12 @@
(cl orig name l a v c r f ff ((cur-ub ... (:break expr)) . ub-rest) (clauses ...) . body))
;; user final
;; This pushes a #t to the user when expression, thus forcing a subloop if a for-clause is found afterwards.
((_ orig name l a v c r f ff ((cur-uw ...) . uw-rest)((:final expr) clauses ...) . body)
(cl orig name l a v c r f ff ((cur-uw ... (:final expr)) . uw-rest) (clauses ...) . body))
((_ orig name l a v c r f ff user ((:final expr) clauses ...) . body)
(final :acc ((_) (expr)) cl-next/acc orig name l a v c r f ff user (clauses ...) . body))
;; User do - sideffecting stuff.
((_ orig name l a v c r f ff ((cur-uw ...) . uw-rest) ((:do expr ...) clauses ...) . body)
(cl orig name l a v c r f ff ((cur-uw ... (:do expr ...)) . uw-rest) (clauses ...) . body))
;; Explicit subloop. Shorthand for (:when #t)
((_ orig name l a v c r f ff ((cur-uw ...) . uw-rest) (:subloop clauses ...) . body)
@ -164,9 +168,9 @@
;; ERROR HANDLING?
((_ orig name l a v c r f ff user (clause . rest) . body)
(syntax-error "Invalid clause in loop" clause orig))
(syntax-error "Invalid clause in loop" clause orig))))
))
;; HOLY CODE-DUPLICATION-BATMAN!
@ -184,15 +188,15 @@
checks
((refs ...))
(finals ...)
ff ul uw ((cur-ub ...) . ub-rest) uf clauses . body)
ff ((cur-ub ...) . ub-rest) clauses . body)
(cl orig name
((lets ... new-lets ...))
((accs ... (accvar accinit accupdate) ...))
((accs ... (accvar accinit accvar) ...))
vars
checks
((refs ... new-refs ...))
(finals ... new-finals ...)
ff ul uw ((cur-ub ... new-checks ...) . ub-rest) uf clauses . body))
ff ((cur-ub ... (:break new-checks) ... (:bind (accvar accupdate) ...)) . ub-rest) clauses . body))
;; We have ONE subloop!
((_ (new-lets ...) ((accvar accinit accupdate) ...) (new-checks ...) (new-refs ...) (new-finals ...)
orig name
@ -202,15 +206,15 @@
checks
((refs ...) . refs-rest)
(finals ...)
ff ul uw ((cur-ub ...) . ub-rest) uf clauses . body)
ff ((cur-ub ...) . ub-rest) clauses . body)
(cl orig name
(lets ... (outermost-lets ... new-lets ...))
((accs ... (accvar accvar accupdate) ...) ((oldacc oldinit oldupdate) ... (accvar accinit accvar) ...))
((accs ... (accvar accvar accvar) ...) ((oldacc oldinit oldupdate) ... (accvar accinit accvar) ...))
vars
checks
((refs ... new-refs ...) . refs-rest)
(finals ... new-finals ...)
ff ul uw ((cur-ub ... new-checks ...) . ub-rest) uf clauses . body))
ff ((cur-ub ... (:break new-checks) ... (:bind (accvar accupdate) ...)) . ub-rest) clauses . body))
;; We have several subloops!
((_ (new-lets ...) ((accvar accinit accupdate) ...) (new-checks ...) (new-refs ...) (new-finals ...)
orig name
@ -220,16 +224,16 @@
checks
((refs ...) . refs-rest)
(finals ...)
ff ul uw ((cur-ub ...) . ub-rest) uf clauses . body)
ff ((cur-ub ...) . ub-rest) clauses . body)
(cl orig name
(lets ... (outermost-lets ... new-lets ...))
((accs ... (accvar accvar accupdate) ...) ((oldacc oldinit oldupdate) ... (accvar accvar accvar) ...) ...
((accs ... (accvar accvar accvar) ...) ((oldacc oldinit oldupdate) ... (accvar accvar accvar) ...) ...
((oldestacc oldestinit oldestupdate) ... (accvar accinit accvar) ...))
vars
checks
((refs ... new-refs ...) . refs-rest)
(finals ... new-finals ...)
ff ul uw ((cur-ub ... new-checks ...) . ub-rest) uf clauses . body))))
ff ((cur-ub ... (:break new-checks) ... (:bind (accvar accupdate) ...)) . ub-rest) clauses . body))))
;; Integrating for clauses is not as involved, since they only want to be introduced into the current
;; loop. Any propagation of for finalizers (ff) is done by push-new-subloop
@ -259,15 +263,13 @@
(define-syntax user
(syntax-rules (:when :bind :break :final :nop)
(syntax-rules (:when :bind :break :do :nop)
((_ final-expr next outer () body ...)
(begin body ...))
((_ f n o (:nop . rest) . body)
(user f n o rest . body))
((_ f n o ((:bind pairs ...) . rest) . body)
(let (pairs ...)
(ref-let (pairs ...)
(user f n o rest . body)))
((_ f n o ((:when test) . rest) . body)
(cond
@ -277,8 +279,12 @@
(cond
(expr final-expr ...)
(else (user (final-expr ...) n o rest . body))))
((_ f n o ((:do expr ...) . rest) . body)
(begin
expr ...
(user f n o rest . body)))))
))
;; If there are no subloops, we emit to the simple case
(define-syntax emit
@ -413,6 +419,7 @@
(user (ff-cur ... ff-above ... final)
(intermediate-loop accstep ... step ...)
#f
(us ...)
(emit-many/rest orig
name
(intermediate-loop accstep ... step ...)
@ -427,14 +434,14 @@
. body)))))))))
(define-syntax forify
(syntax-rules (%acc)
((_ orig name () ((%acc . acc-rest) . argsrest) . body)
(forify* orig name () ((:for ensure-once (up-from 0 1)) (%acc . acc-rest) . argsrest) . body))
((_ . rest)
(forify* . rest))))
(syntax-rules (%acc)
((_ orig name () ((%acc . acc-rest) . argsrest) . body)
(forify* orig name () ((:for ensure-once (up-from 0 1)) (%acc . acc-rest) . argsrest) . body))
((_ . rest)
(forify* . rest))))
(define-syntax forify*
(syntax-rules (:for :acc :when :unless :break :final :subloop :let :let* %acc)
(syntax-rules (:for :acc :when :unless :break :final :subloop :bind :do %acc)
((_ o n done-clauses () . body)
(%loop o n done-clauses . body))
((_ o n (s ...) ((:for c-rest ...) clauses ...) . body)
@ -447,12 +454,12 @@
(forify* o n (s ... (:break expr)) (clauses ...) . body))
((_ o n (s ...) ((:final expr) clauses ...) . body)
(forify* o n (s ... (:final expr)) (clauses ...) . body))
((_ o n (s ...) ((:do expr ...) clauses ...) . body)
(forify* o n (s ... (:do expr ...)) (clauses ...) . body))
((_ o n (s ...) (:subloop clauses ...) . body)
(forify* o n (s ... :subloop) (clauses ...) . body))
((_ o n (s ...) ((:let id id* ... expr) clauses ...) . body)
(forify* o n (s ... (:let id id* ... expr)) (clauses ...) . body))
((_ o n (s ...) ((:let* id id* ... expr) clauses ...) . body)
(forify* o n (s ... (:let* id id* ... expr)) (clauses ...) . body))
((_ o n (s ...) ((:bind pairs ...) clauses ...) . body)
(forify* o n (s ... (:bind pairs ...)) (clauses ...) . body))
((_ o n (s ...) ((%acc c-rest ...) clauses ...) . body)
(forify* o n (s ... (:acc c-rest ...)) (clauses ...) . body))
((_ o n (s ...) ((:acc c-rest ...) clauses ...) . body)

View file

@ -47,7 +47,7 @@
loop/or
loop/list/parallel
:when :unless :break :final :bind :subloop :for :acc
:when :unless :break :final :bind :subloop :do :for :acc
:length :fill
:to :by

View file

@ -508,6 +508,21 @@
((var var))
. rest))))
;; this is an internal "accumulator". It is used for final tests
;; :final in goof differs from in racket. It is lexical, meaning it
;; is tested where it is placed in the clauses, and any subloop is
;; executed completely.
(define-syntax final
(syntax-rules (:acc)
((_ :acc ((var) (test)) n . rest)
(n ()
((final #f test))
(final)
()
()
. rest))))
;;; Here starts generator clauses.
(define (generator->list gen)

View file

@ -37,8 +37,8 @@
a)
'(0 1 2 3))
(test-equal ":let and :let*"
(loop/list ((a (up-from 0 5)) (:let b (+ a 1)) (:let* c (+ b 1)))
(test-equal ":bind"
(loop/list ((a (up-from 0 5)) (:bind (b (+ a 1))) (:bind (c (+ b 1))))
c)
'(2 3 4 5 6))