Loops: :finish, :abandon, :while, :until and the :subloop form
The clause table, stopping early and nesting use the new names, and count-lines uses :until instead of a counter that only drove the loop. 🤖 Generated with [ECA](https://eca.dev) (anthropic/claude-opus-5-5) Co-Authored-By: eca-agent <git@eca.dev>
This commit is contained in:
parent
d35a116b8a
commit
1fe22a4dfd
2 changed files with 94 additions and 10 deletions
|
|
@ -1312,15 +1312,19 @@ The clauses are:
|
||||||
|---------------------------------+---------------------------------------------------------------------|
|
|---------------------------------+---------------------------------------------------------------------|
|
||||||
| @code{(:for pat coll)} | binds @code{pat} to each element of @code{coll} in turn |
|
| @code{(:for pat coll)} | binds @code{pat} to each element of @code{coll} in turn |
|
||||||
| @code{(:with pat start update)} | a variable that starts at @code{start} and is updated every round |
|
| @code{(:with pat start update)} | a variable that starts at @code{start} and is updated every round |
|
||||||
|
| @code{(:while test)} | keeps going while @code{test} holds, checked before every round |
|
||||||
|
| @code{(:until test)} | keeps going until @code{test} holds, checked before every round |
|
||||||
| @code{(:let pat expr)} | binds something for the rest of this round |
|
| @code{(:let pat expr)} | binds something for the rest of this round |
|
||||||
| @code{(:when test)} | skips the rest of this round unless @code{test} holds |
|
| @code{(:when test)} | skips the rest of this round unless @code{test} holds |
|
||||||
| @code{(:when-let pat expr)} | binds, and skips the round when the pattern does not match |
|
| @code{(:when-let pat expr)} | binds, and skips the round when the pattern does not match |
|
||||||
| @code{(:break test)} | ends the loop right away when @code{test} holds |
|
| @code{(:finish test [value])} | ends the loop right away when @code{test} holds |
|
||||||
| @code{(:break-let pat expr)} | binds, and ends the loop when the pattern does not match |
|
| @code{(:finish-let pat expr)} | binds, and ends the loop when the pattern does not match |
|
||||||
| @code{(:final test)} | ends the loop after this round when @code{test} holds |
|
| @code{(:final test)} | ends the loop after this round when @code{test} holds |
|
||||||
|
| @code{(:abandon test)} | ends a nested loop, and goes on with the next round of the outer one |
|
||||||
| @code{(:do expr ...)} | runs something for its effect |
|
| @code{(:do expr ...)} | runs something for its effect |
|
||||||
| @code{(:acc name (collector x))} | collects @code{x} |
|
| @code{(:acc name (collector x))} | collects @code{x} |
|
||||||
| @code{(:subloop)} | starts a nested loop |
|
| @code{(:subloop)} | between two @code{:for}s: nests them |
|
||||||
|
| @code{(:subloop clause ...)} | a loop inside the round, whose accumulators the round can use |
|
||||||
| @code{=> expr} | the value of the loop (last, optional) |
|
| @code{=> expr} | the value of the loop (last, optional) |
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1390,6 +1394,51 @@ Two @code{:for} clauses in a row walk their collections side by side, and the lo
|
||||||
=> out) ;; '(11 21 12 22)
|
=> out) ;; '(11 21 12 22)
|
||||||
}|
|
}|
|
||||||
|
|
||||||
|
@code{(:abandon test)} in the inner loop ends it early, and the outer loop goes on with its next round. In the outer loop there is nothing to go on with, so there it is an error:
|
||||||
|
|
||||||
|
@codeblock[#:lang "bjolang"]|{
|
||||||
|
(loop (:for x (list 1 2 3))
|
||||||
|
(:subloop)
|
||||||
|
(:for y (list 1 2 3))
|
||||||
|
(:abandon (> y x))
|
||||||
|
(:acc out (listing (x . y)))
|
||||||
|
=> out) ;; '((1 . 1) (2 . 1) (2 . 2) (3 . 1) (3 . 2) (3 . 3))
|
||||||
|
}|
|
||||||
|
|
||||||
|
A nested loop like this runs to the end of the round: the clauses after the inner @code{:for} belong to the inner loop. When the round should go on after the inner loop, with what it computed, write the inner loop as a @code{:subloop} form instead. It is a loop of its own inside the round, and its accumulators are there for the clauses after it, with their final values:
|
||||||
|
|
||||||
|
@codeblock[#:lang "bjolang"]|{
|
||||||
|
(loop (:for row (list (list 1 2) (list 3 4 5)))
|
||||||
|
(:subloop (:for x row)
|
||||||
|
(:acc sum (summing x))
|
||||||
|
(:acc n (counting x)))
|
||||||
|
(:acc out (listing (sum . n)))
|
||||||
|
=> out) ;; '((3 . 2) (12 . 3))
|
||||||
|
}|
|
||||||
|
|
||||||
|
With @code{=> (name expr)} at its end, only @code{name} is bound after the form, to @code{expr}:
|
||||||
|
|
||||||
|
@codeblock[#:lang "bjolang"]|{
|
||||||
|
(loop (:for row rows)
|
||||||
|
(:subloop (:for x row)
|
||||||
|
(:acc sum (summing x))
|
||||||
|
(:acc n (counting x))
|
||||||
|
=> (avg (if (= n 0) 0 (/ sum n))))
|
||||||
|
(:acc out (listing avg))
|
||||||
|
=> out)
|
||||||
|
}|
|
||||||
|
|
||||||
|
The form's accumulators start over every time it runs, and a form that runs zero times leaves them at their start values. Its own variables are never visible after it. Inside it, two clauses are about the form:
|
||||||
|
|
||||||
|
@read-list{
|
||||||
|
- @code{(:finish-subloop test)} ends the form here, and the round goes on after it with the accumulators as they are.
|
||||||
|
- @code{(:abandon-subloop test)} throws the form and the rest of the round away, and the outer loop goes on with its next round.
|
||||||
|
}
|
||||||
|
|
||||||
|
@code{:when}, @code{:abandon}, @code{:while} and @code{:until} inside the form are about the form's own rounds. @code{:finish} still ends the whole loop.
|
||||||
|
|
||||||
|
A variable inside a loop cannot reuse the name of an accumulator, of a @code{:with}, or of a variable of an outer loop: @code{(:for x xs) (:subloop) (:for x x)} is an error. Pick another name.
|
||||||
|
|
||||||
@subsubsection{State that carries over: :with}
|
@subsubsection{State that carries over: :with}
|
||||||
|
|
||||||
@code{(:with name start update)} is a variable that starts at @code{start} and is set to @code{update} after every round. All the @code{:with} updates of a round happen at once, so each of them sees the old values of the others:
|
@code{(:with name start update)} is a variable that starts at @code{start} and is set to @code{update} after every round. All the @code{:with} updates of a round happen at once, so each of them sees the old values of the others:
|
||||||
|
|
@ -1405,10 +1454,10 @@ Two @code{:for} clauses in a row walk their collections side by side, and the lo
|
||||||
|
|
||||||
@subsubsection{Stopping early}
|
@subsubsection{Stopping early}
|
||||||
|
|
||||||
@code{:break} leaves the loop before the rest of the round runs, and @code{:final} leaves it after. @code{:when-let} and @code{:break-let} bind a pattern that may fail, and skip the round or leave the loop when it does:
|
@code{:finish} leaves the loop before the rest of the round runs, and @code{:final} leaves it after. @code{:when-let} and @code{:finish-let} bind a pattern that may fail, and skip the round or leave the loop when it does. The names follow one rule: @em{finish} stops and goes on @em{after} the loop, @em{abandon} stops and goes @em{up} to the next round of the outer loop.
|
||||||
|
|
||||||
@codeblock[#:lang "bjolang"]|{
|
@codeblock[#:lang "bjolang"]|{
|
||||||
(loop (:for x (list 1 2 3 4 5)) (:break (> x 3)) (:acc out (listing x)) => out)
|
(loop (:for x (list 1 2 3 4 5)) (:finish (> x 3)) (:acc out (listing x)) => out)
|
||||||
;; '(1 2 3)
|
;; '(1 2 3)
|
||||||
|
|
||||||
(loop (:for x (list 1 2 3 4 5)) (:final (> x 3)) (:acc out (listing x)) => out)
|
(loop (:for x (list 1 2 3 4 5)) (:final (> x 3)) (:acc out (listing x)) => out)
|
||||||
|
|
@ -1422,13 +1471,48 @@ Two @code{:for} clauses in a row walk their collections side by side, and the lo
|
||||||
;; 4
|
;; 4
|
||||||
|
|
||||||
;; Number the lines of a port until it is empty. up-from without a #:to
|
;; Number the lines of a port until it is empty. up-from without a #:to
|
||||||
;; never stops on its own, so the :break-let is what ends the loop.
|
;; never stops on its own, so the :finish-let is what ends the loop.
|
||||||
(loop (:for line-number (up-from 1))
|
(loop (:for line-number (up-from 1))
|
||||||
(:break-let (Some line) (read-line/opt port))
|
(:finish-let (Some line) (read-line/opt port))
|
||||||
(:do (println #"${line-number}: ${line}")))
|
(:do (println #"${line-number}: ${line}")))
|
||||||
}|
|
}|
|
||||||
|
|
||||||
A loop has to start with a @code{:for} or a @code{:with}, since the other clauses belong to the round that it opens.
|
@code{(:finish test value)} leaves the loop with @code{value} as its result, and the @code{=>} expression is not run. The value has to have the same type as the @code{=>} expression:
|
||||||
|
|
||||||
|
@codeblock[#:lang "bjolang"]|{
|
||||||
|
;; The sum, or -1 as soon as a negative number shows up.
|
||||||
|
(loop (:for x (list 1 2 -3 4))
|
||||||
|
(:finish (< x 0) -1)
|
||||||
|
(:acc total (summing x))
|
||||||
|
=> total)
|
||||||
|
;; -1
|
||||||
|
}|
|
||||||
|
|
||||||
|
@code{:break} and @code{:break-let} are the old names of @code{:finish} and @code{:finish-let}. They still work, with a warning.
|
||||||
|
|
||||||
|
@subsubsection{Looping while something holds: :while and :until}
|
||||||
|
|
||||||
|
@code{(:while test)} keeps the loop going while @code{test} holds, and @code{(:until test)} until it does. Neither binds anything, so a loop that has no collection to walk can start with one:
|
||||||
|
|
||||||
|
@codeblock[#:lang "bjolang"]|{
|
||||||
|
(loop (:until (port-eof? port))
|
||||||
|
(:let line (read-line port))
|
||||||
|
(:do (println line)))
|
||||||
|
}|
|
||||||
|
|
||||||
|
The test is checked before every round, the first one too, so the loop may run zero times. Next to a @code{:for}, the loop ends at whichever stops first, and the test can use the @code{:for}'s variable:
|
||||||
|
|
||||||
|
@codeblock[#:lang "bjolang"]|{
|
||||||
|
(loop (:for x (list 1 2 3 4 5))
|
||||||
|
(:until (> x 3))
|
||||||
|
(:acc out (listing x))
|
||||||
|
=> out)
|
||||||
|
;; '(1 2 3)
|
||||||
|
}|
|
||||||
|
|
||||||
|
Since the test runs before the round, it cannot see what the round computes. To stop on that, put a @code{:finish} after it: @code{(:let line (read-line/opt port)) (:finish (none? line))}.
|
||||||
|
|
||||||
|
A loop has to start with a @code{:for}, a @code{:with}, a @code{:while} or an @code{:until}, since the other clauses belong to the round that it opens.
|
||||||
|
|
||||||
@subsubsection{Counting and walking part of a collection}
|
@subsubsection{Counting and walking part of a collection}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -500,8 +500,8 @@ To avoid leaking resources, you will usually want to use @code{call-with-input-f
|
||||||
(defun (count-lines path)
|
(defun (count-lines path)
|
||||||
(call-with-input-file path
|
(call-with-input-file path
|
||||||
(fun (port)
|
(fun (port)
|
||||||
(loop (:for i (up-from 0))
|
(loop (:until (port-eof? port))
|
||||||
(:break-let (Some line) (read-line/opt port))
|
(:let line (read-line port))
|
||||||
(:acc lines (counting line))
|
(:acc lines (counting line))
|
||||||
=> lines))))
|
=> lines))))
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue