diff --git a/_01.Language-basics.sz b/_01.Language-basics.sz index db3e2f2..2d1aa99 100644 --- a/_01.Language-basics.sz +++ b/_01.Language-basics.sz @@ -1312,15 +1312,19 @@ The clauses are: |---------------------------------+---------------------------------------------------------------------| | @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{(: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{(: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{(:break test)} | 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 test [value])} | ends the loop right away when @code{test} holds | +| @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{(: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{(: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) | } @@ -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) }| +@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} @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} -@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"]|{ -(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) (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 ;; 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)) - (:break-let (Some line) (read-line/opt port)) + (:finish-let (Some line) (read-line/opt port)) (: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} diff --git a/_02_.Writing-programs.sz b/_02_.Writing-programs.sz index 8a5ab52..4664f8c 100644 --- a/_02_.Writing-programs.sz +++ b/_02_.Writing-programs.sz @@ -500,8 +500,8 @@ To avoid leaking resources, you will usually want to use @code{call-with-input-f (defun (count-lines path) (call-with-input-file path (fun (port) - (loop (:for i (up-from 0)) - (:break-let (Some line) (read-line/opt port)) + (loop (:until (port-eof? port)) + (:let line (read-line port)) (:acc lines (counting line)) => lines))))