261 lines
14 KiB
XML
261 lines
14 KiB
XML
![]() |
<doc>
|
||
|
<metadata>
|
||
|
<title></title>
|
||
|
<author email="linus.internet@fastmail.se">Linus Björnstam</author>
|
||
|
</metadata>
|
||
|
<section title="Preface">
|
||
|
<p>
|
||
|
We have all had the thought. <em>Why oh why are not all our problems solved by a nice recursive algorithm over a list?</em>. We see a neat problem. We smile and think about all the nice things we shall do right after finishing off this little tidbit, this delicious little bite of a problem. Then we scrape the surface. Under there is a jungle of state. Suddenly our little recursive list-mangling is not enough. To defeat the anxiety rising within us, we quickly write something using map, filter, zip and reduce. We test it. We smile again. We test it on real world data. The smile stiffens. The little bite has turned in to an o(n²) turd.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
We raise our gaze and see the disgustingly mutating little wonders we could have achieved using python list comprehensions. We look sideways and see the magnificent, fantastic, but abominable loop macro in Common Lisp. Even they, the elitist ultra-productive scumbags, hate it, yet they sieze every moment to make underhanded comments about named lets. Why, oh why is the world not as elegant as we want it to be?
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
We think again about how python programmers write scheme, and shudder. <em>"No! Never that!"</em>, we think to ourselves. A vague idea starts forming. A quote surfaces from the depths of our minds <em>"Above all the wonders of Lisp's pantheon stand its metalinguistic tools; by their grace have Lisp's acolytes been liberated from the rigid ascetism of lesser faiths"</em>. Maybe we should try to do it ourselves. Thinking about it, we have actually written some macros before. Anaphoric ifs, some boilerplate removal macros, and oh! Even an almost-working implementation of SRFI-26. It passed nearly all the tests, and we couldn't be bothered to go any further. But a looping facility... How hard can it be?
|
||
|
</p>
|
||
|
<p>
|
||
|
-- Linus Björnstam, September 2020
|
||
|
</p>
|
||
|
|
||
|
</section>
|
||
|
|
||
|
<section title="Introduction">
|
||
|
<p>
|
||
|
Goof-loop is like the name suggests a looping facility. It is based in many ways on things I learned porting Racket's for-loops to guile and on the marvel that is (chibi loop) in chibi-scheme. The goals of goof-loop are:
|
||
|
<ul>
|
||
|
<li> Portability. Any sufficiently masochistic programmer should be able to implement it in r7rs syntax-rules</li>
|
||
|
<li> A coherent, simple interface for looping over various kinds of data </li>
|
||
|
<li> A coherent, simple intefrace for accumulating data in loops, with support for accumulating data over sub-loops </li>
|
||
|
<li> A looping facility that in almost all cases produces as fast code as a hand-written named let </li>
|
||
|
<li> An extensible looping facility, where new ways of iterating over data can be easily added </li>
|
||
|
</ul>
|
||
|
|
||
|
The only other lisp looping facility that I know of that provides these things is Common Lisps iterate package. Iterate does however do a lot of things that are cumbersome to do in portable scheme, and would be prohibitively complicated to implement efficiently in a portable way.
|
||
|
</p>
|
||
|
|
||
|
<subsection title="An example or two">
|
||
|
<p>
|
||
|
So, how does it look? A slightly contrived example, a naive sieve of Erathostenes:
|
||
|
<example>
|
||
|
(define (erathostenes n)
|
||
|
(define vec (make-vector n #t))
|
||
|
(loop/list ((:for i (up-from 2 (to n)))
|
||
|
:when (vector-ref vec i))
|
||
|
(loop ((:for j (up-from (* 2 i) (to n) (by i))))
|
||
|
(vector-set! vec j #f))
|
||
|
i))
|
||
|
</example>
|
||
|
|
||
|
Calling <code>(erathostenes 10) </code> returns a list of all primes below 10.
|
||
|
</p>
|
||
|
</subsection>
|
||
|
</section>
|
||
|
|
||
|
<section title="Specification">
|
||
|
<p>
|
||
|
The loop grammar is the following:
|
||
|
<example>
|
||
|
(loop [name] (loop-clause ...) [=> final-expr] body ...)
|
||
|
|
||
|
name = identifier
|
||
|
|
||
|
loop-clause = (:for id id* ... seq-expr)
|
||
|
| (:acc id id* ... seq-expr)
|
||
|
| :when guard-expr
|
||
|
| :unless guard-expr
|
||
|
| :break break-expr
|
||
|
| :final guard-expr
|
||
|
| :subloop
|
||
|
</example>
|
||
|
|
||
|
If a <code>name</code> is provided, it will be bound to a macro that allows named update of loop variables:
|
||
|
|
||
|
<example>
|
||
|
(loop lp ((:for a (in 0 (+ a 1)))
|
||
|
:break (> a 9))
|
||
|
=> '()
|
||
|
(if (= 4 a)
|
||
|
(cons a (lp (=> a 8)))
|
||
|
(cons a (lp))))
|
||
|
</example>
|
||
|
|
||
|
This rather inane example would return <code>(0 1 2 3 4 8 9)</code>.
|
||
|
</p>
|
||
|
<subsection title="Subloops">
|
||
|
A subloop is a distinction between an outer loop and an inner loop. An for each element yielded by an outer loop, the inner loop is run until exhaustion. All non :for- or :acc-clauses break out a subloop.
|
||
|
|
||
|
<example>
|
||
|
(loop ((:for a (in-list '(1 2 3)))
|
||
|
:subloop
|
||
|
(:for b (up-from 0 (to a)))
|
||
|
(:acc acc (listing (cons a b))))
|
||
|
=> acc)
|
||
|
;; => ((1 . 0) (2 . 0) (2 . 1) (3 . 0) (3 . 1) (3 . 2))
|
||
|
</example>
|
||
|
|
||
|
The above <code>:subloop</code> clause is equivalent to <code>:when #t</code> and <code> :unless #f</code>. A <code>:break </code> clause will immediately stop execution of the loop:
|
||
|
|
||
|
<example>
|
||
|
(loop ((:for a (in-list '(1 2 3)))
|
||
|
:break (= 3 a)
|
||
|
(:for b (up-from 0 (to a)))
|
||
|
(:acc acc (listing (cons a b))))
|
||
|
=> acc)
|
||
|
;; => ((1 . 0) (2 . 0) (2 . 1))
|
||
|
</example>
|
||
|
|
||
|
And a :final guard will let one more body be evaluated. This clause is evaluated in the innermost loop, and as such only one body will be evaluated:
|
||
|
|
||
|
<example>
|
||
|
(loop ((:for a (in-list '(1 2 3)))
|
||
|
:final (= 3 a)
|
||
|
(:for b (up-from 0 (to a)))
|
||
|
(:acc acc (listing (cons a b))))
|
||
|
=> acc)
|
||
|
;; =>((1 . 0) (2 . 0) (2 . 1) (3 . 0))
|
||
|
</example>
|
||
|
</subsection>
|
||
|
<subsection title="Simple forms">
|
||
|
The pure <code>loop</code> macro is quite a big hammer for most tasks. Often we want to do simple things, like collect elements into a list or a vector, which means the extra housekeeping of separating accumulators and for clauses are too much heavy lifting. goof-loop provides several simpler forms that can be used in those cases. In these simpler forms :acc is disallowed, and everything not identified as anything else is assumed to be a :for clause. The loop below accumulates the 100 first fibonacci numbers into a list.
|
||
|
|
||
|
<example>
|
||
|
(loop/list ((count (up-from 0 (to 100)))
|
||
|
(a (in 0 b))
|
||
|
(b (in 1 (+ a b))))
|
||
|
b)
|
||
|
</example>
|
||
|
|
||
|
|
||
|
<dl>
|
||
|
<dt type="syntax">(loop/first (clauses ...) body ...)</dt>
|
||
|
<dd>
|
||
|
<p>
|
||
|
If any body is ever evaluated, stop and return the value of that evaluation. If no body is ever evaluated the return value is unspecified.
|
||
|
</p>
|
||
|
</dd>
|
||
|
|
||
|
<dt type="syntax">(loop/last (clauses ...) body ...)</dt>
|
||
|
<dd>
|
||
|
<p>
|
||
|
Returns the result of the last body to be evaluated. If no body is evaluated the return value is unspecified.
|
||
|
</p>
|
||
|
</dd>
|
||
|
|
||
|
<dt type="syntax">(loop/list (clauses ...) body ...)</dt>
|
||
|
<dd>
|
||
|
<p>
|
||
|
Iterates over <code>clauses</code> and builds a list of the result of every evaluation of body. The order of the list is the same as the order body was evaluated in. The result of the first evaluation of body is the first element of the resulting list.
|
||
|
</p>
|
||
|
<p>
|
||
|
The list returned is the same even when used with multi-shot continuations
|
||
|
</p>
|
||
|
<p>
|
||
|
If no body is evaluated, the result is the empty list
|
||
|
</p>
|
||
|
</dd>
|
||
|
|
||
|
<dt type="syntax">(loop/product (clauses ...) body ...)</dt>
|
||
|
<dd>
|
||
|
<p>
|
||
|
Multiplies each evaluated body. If no body is evaluated, 1 is returned
|
||
|
</p>
|
||
|
</dd>
|
||
|
|
||
|
<dt type="syntax">(loop/sum (clauses ...) body ...)</dt>
|
||
|
<dd>
|
||
|
<p>
|
||
|
Sums the result of each evaluated body. If no body is evaluated, 0 is returned.
|
||
|
</p>
|
||
|
</dd>
|
||
|
|
||
|
<dt type="syntax">(loop/and (clauses ...) body ...)</dt>
|
||
|
<dd>
|
||
|
<p>
|
||
|
If all evaluated bodies return truthy, return the result of the last evaluated body. If any body returns #f, stop iteration and return #f. If no body is evaluated, #t is returned.
|
||
|
</p>
|
||
|
</dd>
|
||
|
|
||
|
<dt type="syntax">(loop/or (clauses ...) body ...)</dt>
|
||
|
<dd>
|
||
|
<p>
|
||
|
If any evaluated body returns truthy, stop iteration and return the result of that body. If no body returns truthy, return #f. If no body is evaluated, return #f.</p>
|
||
|
</dd>
|
||
|
|
||
|
<dt type="syntax">(loop/list/parallel (clauses ...) body ...)</dt>
|
||
|
<dd>
|
||
|
<p>
|
||
|
Like loop/list, but evaluates each body in parallel.
|
||
|
</p>
|
||
|
</dd>
|
||
|
|
||
|
</dl>
|
||
|
|
||
|
</subsection>
|
||
|
|
||
|
|
||
|
<subsection title=":for-clauses">
|
||
|
<dl>
|
||
|
<dt type="syntax">(:for identifier (in start [update [stop]]))</dt>
|
||
|
<dd>
|
||
|
<p>
|
||
|
Binds a loop variable to <code>identifier</code>. It's first value is <code>start</code>. It is updated by the <code>update</code> expression, or is left unchanged if no such expression is present. If a <code>stop</code> expression is provided, it will be evaluated before each loop body. If the <code>stop</code> expression returns true, the iteration will be considered exhausted.
|
||
|
|
||
|
<example>
|
||
|
(loop ((:for a (in 0 b)) (:for b (in 1 (+ a b) (> b 20))))
|
||
|
(display b) (newline))
|
||
|
</example>
|
||
|
Will print all fibonacci numbers below 20.
|
||
|
</p>
|
||
|
</dd>
|
||
|
|
||
|
<dt type="syntax">(:for identifier (up-from start [(to bound)] [(by step)])</dt>
|
||
|
<dt type="syntax">(:for identifier (up-from start [bound [by]]))</dt>
|
||
|
<dd>Binds <code>identifier</code> to the number <code>start</code> up to <code>bound</code> (exclusive!) by <code>step</code>. If no <code>bound</code> is given, it will yield values indefinitely. The second shorter form will not allow unbounded iteratiom.</dd>
|
||
|
|
||
|
<dt type="syntax">(:for identifier (down-from start [(to bound)] [(by step)])</dt>
|
||
|
<dt type="syntax">(:for identifier (down-from start [bound [by]]))</dt>
|
||
|
<dd>Binds <code>identifier</code> to the number <code>start</code> down to <code>bound</code> (inclusive!) by <code>step</code>. If no <code>bound</code> is given, it will yield values indefinitely. The second shorter form will not allow unbounded iteratiom.</dd>
|
||
|
|
||
|
<dt type="syntax">(:for identifier [pair] (in-list expr [by])</dt>
|
||
|
<dd>Binds <code>identifier</code> to the car of the loop variable <code>pair</code>. <code>pair</code> is advanced by applying the procedure <code>by</code> to it (defaulting to <code>cdr</code>). The iteration stops when <code>pair</code> is the empty list.</dd>
|
||
|
|
||
|
<dd type="syntax">(:for identifier [pairs] (in-lists expr [by])</dd>
|
||
|
<dt>Works the same as <code>in-list</code>, but <code>expr</code> must evaluate to a list of lists. <code>identifier</code> is bound to the car of those lists, and they are advanced by <code>by</code>, defaulting to <code>cdr</code>.</dt>
|
||
|
|
||
|
<dd type="syntax">(:for identifier [index] (in-vector expr [low [high]]))</dd>
|
||
|
<dt>Binds <code>identifier</code> to all elements in the vector produced by <code>expr</code> in order from <code>low</code> to <code>high</code>. <code>low</code> defaults to 0 and <code>high</code> defaults to the last index of the vector.</dt>
|
||
|
|
||
|
<dd type="syntax">(:for identifier [index] (in-reverse-vector expr [high [low]]))</dd>
|
||
|
<dt>Binds <code>identifier</code> to all elements in the vector produced by <code>expr</code> in reverse order from <code>high</code> to <code>low</code>. <code>high</code> defaults to the last element of the vector and <code>low</code> defaults to 0.</dt>
|
||
|
|
||
|
<dd type="syntax">(:for identifier [index] (in-string expr [low [high]]))</dd>
|
||
|
<dt>Binds <code>identifier</code> to all elements in the string produced by <code>expr</code> in order from <code>low</code> to <code>high</code>. <code>low</code> defaults to 0 and <code>high</code> defaults to the last index of the string.</dt>
|
||
|
|
||
|
<dd type="syntax">(:for identifier [index] (in-reverse-string expr [high [low]]))</dd>
|
||
|
<dt>Binds <code>identifier</code> to all elements in the vector produced by <code>expr</code> in reverse order from <code>high</code> to <code>low</code>. <code>high</code> defaults to the last element of the vector and <code>low</code> defaults to 0.</dt>
|
||
|
|
||
|
<dd type="syntax">(:for identifier (in-port port [reader [eof?]]))</dd>
|
||
|
<dt>Binds <code>identifier</code> to the result of calling <code>reader</code> on <code>port</code>. Iteration stops when <code>(eof? identifier)</code> returns true.</dt>
|
||
|
|
||
|
<dd type="syntax">(:for identifier (in-file path [reader [eof?]]))</dd>
|
||
|
<dt>Opens the file located at <code>path</code> (which is a string) and binds <code>identifier</code> to the result of calling <code>reader</code> on the opened port. Iteration stops when <code>(eof? identifier)</code> returns true.</dt>
|
||
|
|
||
|
<dd type="syntax">(:for identifier (in-generator gen))</dd>
|
||
|
<dt>Binds identifier to the result of calling the SRFI-158-compatible generator <code>gen</code>. Iteration stops when <code>gen</code> returns the end-of-file object.</dt>
|
||
|
|
||
|
</dl>
|
||
|
|
||
|
</subsection>
|
||
|
|
||
|
<subsection title=":acc-clauses">
|
||
|
|
||
|
</subsection>
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
</section>
|
||
|
</doc>
|