4 Locals and flow
Builds on: Chapter 3 (signatures, named inputs).
Questions this chapter answers
- When is a local made, and how far is it visible?
- How are
if,loopandcasewritten, and what must they keep about the stack? - What is the short way to write a loop over an array?
- How is a word given as a value?
4.1 Locals
In a body, to NAME puts the top value into that name. The first time a name is used, a local of the value’s type is made; there is no separate line that declares it.
04-locals.pnx
use io
word describe (a int, b int, c int gives str)
a b add c add to total
total 3 div to mean
a b max c max to top
cat ("total ", total, ", mean ", mean, ", largest ", top)
end
3 9 6 describe io.println
Output
$ pnx 04-locals.pnx
total 18, mean 6, largest 9
to takes the top value off the stack and keeps it in the name. Using the name puts the value back on. Instead of holding values on the stack for long, keep them in names and take them out when needed.
| Token | Stack after it | What happened |
|---|---|---|
a b add | 12 | a is 3, b is 9. |
c add | 18 | c is 6. |
to total | — | Takes 18 off and keeps it in a new local total. The stack is empty. |
total 3 div | 6 | Using total puts 18 on; it is divided by 3. |
to mean | — | Keeps 6 in mean. |
A local is visible from where it is first stored to the end of the branch, loop or block around it. A name first made inside a branch cannot be read outside it: coming by the other branch, it would hold no value.
04-scope.pnx
use io
word f (n int gives int)
n 0 gt if n 2 mul to doubled end
doubled
end
Refused
$ pnx 04-scope.pnx
04-scope.pnx:4:3: this name is not declared before this place
A global is stored with set, not to; to stores only into locals.
04-store-global.pnx
use io
global total int
word add_to_total (n int) total n add set total end rem set: a global is written with set
3 add_to_total
4 add_to_total
cat ("total ", total) io.println
Output
$ pnx 04-store-global.pnx
total 7
Common mistake: Storing into a global with to total
to with the name of a global is refused, and the message names set. A global cannot be hidden, so no local of that name can be made; the rule also keeps a to meant to make a local from changing a global silently.04-store-global-bare.pnx
global total int
word add_to_total (n int) total n add to total end
Refused
$ pnx 04-store-global-bare.pnx
04-store-global-bare.pnx:2:42: this name is a global; a global is written with set: value set name
4.2 Branches
condition if … else … end looks at the bool on top and runs one side. Both branches shall leave the same stack; a branch that leaves with return does not need to match.
04-if.pnx
use io
word grade (score int gives str)
score 90 ge if "A" return end
score 80 ge if "B" return end
score 70 ge if "C" else "F" end
end
95 grade io.println
82 grade io.println
40 grade io.println
Output
$ pnx 04-if.pnx
A
B
F
if takes the bool off the top and chooses the way. The two branches have to leave the same stack because the code after end does not know which branch it came from. Whichever way it came, the stack must be the same for the processor to check what follows.
04-sign.pnx
use io
word sign (n int gives str)
n 0 lt if rem lt leaves a bool; if takes it off the stack
"negative" rem this way leaves one str
else
n 0 eq if "zero" else "positive" end rem and so does this way, whichever inner way it takes
end rem both ways meet here with one str on the stack
end
-4 sign io.println
0 sign io.println
9 sign io.println
Output
$ pnx 04-sign.pnx
negative
zero
positive
| Token | Stack after it | What happened |
|---|---|---|
n 0 lt | true | With n at −4 the comparison leaves true. |
if | — | Takes the bool off and goes to the first branch. |
"negative" | "negative" | This branch leaves one str. |
end | "negative" | The else branch also leaves one str, so the two ways agree. |
An if without else shall leave the stack as it was. Leaving out the else of an if that leaves a value is refused.
04-if-unequal.pnx
use io
word label (n int gives str) n 0 lt if "negative" end end
Refused
$ pnx 04-if-unequal.pnx
04-if-unequal.pnx:2:51: an if without else must leave the stack as it found it
4.3 Loops
loop … end repeats. Inside it, condition until leaves when the condition is true, condition while when it is false, and break without a condition. One turn shall leave the stack as it found it — so the values a loop makes are gathered in locals.
04-loop.pnx
use io
word collatz (n int gives int) rem steps to reach 1
0 to steps
loop
n 1 eq until
n 2 mod 0 eq if n 2 div else n 3 mul 1 add end to n
steps 1 add to steps
end
steps
end
cat ("27 takes ", 27 collatz, " steps") io.println
Output
$ pnx 04-loop.pnx
27 takes 111 steps
One turn leaves the stack as it found it for the same reason. The head of the loop sees the stack both when the loop is first entered and when a turn comes back, and both must be the same. If each turn piled up one more value, the processor, which cannot know how many turns there will be, could not fix the depth of the stack. So the values a loop makes are gathered in locals.
04-loop-sum.pnx
use io
use str
word sum_to (n int gives int)
0 to total rem what the loop builds lives in locals, not on the stack
1 to i
loop
i n gt until rem leave once i passes n
total i add to total rem the stack is empty again at the end of the turn
i 1 add to i
end
total
end
10 sum_to str.to_str io.println
Output
$ pnx 04-loop-sum.pnx
55
04-loop-leaves.pnx
word count_up (n int gives int)
0 to i
loop
i n ge until
i 1 add rem a new value each turn, and nothing takes it
end
i
end
Refused
$ pnx 04-loop-leaves.pnx
04-loop-leaves.pnx:6:3: one turn of a loop must leave the stack as it found it
count_up makes a value with i 1 add every turn, and nothing takes it. With to i after it the value goes into i and the stack is as it was at the start of the turn.
A named loop can be left all at once from an inner loop.
04-named-loop.pnx
use io
word is_prime (n int gives bool)
n 2 lt if false return end
2 to d
loop
d d mul n gt if true return end
n d mod 0 eq if false return end
d 1 add to d
end
end
word first_pair (target int gives str) rem the first a < b <= 20, both prime, with a + b = target
2 to a
0 to b
loop (outer)
a 1 add to b
loop
b 20 gt until
a is_prime b is_prime and a b add target eq and until (outer)
b 1 add to b
end
a 1 add to a
a 20 gt if "none" return end
end
cat (a, " + ", b)
end
26 first_pair io.println
Output
$ pnx 04-named-loop.pnx
7 + 19
Q&A
Q. Can I give the number of loops to leave instead?
A. No. With a number, wrapping a loop in one more would quietly leave another loop. A name cannot do that.
4.4 Choosing
case compares an int or str with the values of its branches. Several values for one branch go in parentheses. What to do when nothing matches goes in otherwise; with no otherwise and no match, it is a fault.
04-case.pnx
use io
word day_kind (day str gives str)
day case
("sat", "sun") of "weekend" end
("mon", "tue", "wed", "thu", "fri") of "weekday" end
otherwise "not a day" end
end
end
"sun" day_kind io.println
"wed" day_kind io.println
"xyz" day_kind io.println
Output
$ pnx 04-case.pnx
weekend
weekday
not a day
The values of the branches are literals or consts of this source. Constants of modules such as err.full or json.key cannot be case values; compare them with eq.
4.5 Stems and blocks
A loop over an array or a list is written with a stem and a combining word instead. The name of the array is the stem, and a do … end block after it, with a word such as each, count or fold, takes it. The block is expanded in place, so it can use the locals around it.
04-stems.pnx
use io
use str
array temps int 7
assign temps (12, 15, 9, 21, 18, 7, 14)
cat ("sum ", temps sum, ", above 13: ", temps do (t int gives bool) t 13 gt end count) io.println
cat ("max ", temps maximum or_else (e) 0 end, ", min ", temps minimum or_else (e) 0 end) io.println
temps do (t int, i int) cat ("day ", i, ": ", t) io.println end each_index
word spread (gives int) 0 temps do (acc int, t int gives int) acc t 10 sub abs add end fold end
cat ("distance from 10, added up: ", spread) io.println
Output
$ pnx 04-stems.pnx
sum 96, above 13: 4
max 21, min 7
day 0: 12
day 1: 15
day 2: 9
day 3: 21
day 4: 18
day 5: 7
day 6: 14
distance from 10, added up: 34
lo hi range is a stem too (hi is not included). fold takes a first value before the stem, and its block takes (the value so far, an element) and gives the new value.
The block of each, each_index or times is left early with break, while or until, as a loop is: the stem stops and the code after the word runs. A search that stops at the first hit is written so.
04-leave-each.pnx
use io
array prices int 6
word load_prices ()
0 6 range do (i int) i 7 mul 3 add i set prices end each
end
word first_over (limit int gives r int)
-1 to found
prices do (p int, i int)
p limit gt if i to found break end
end each_index
found
end
load_prices
cat ("first over 20 at ", 20 first_over) io.println
cat ("first over 99 at ", 99 first_over) io.println
Output
$ pnx 04-leave-each.pnx
first over 20 at 3
first over 99 at -1
4.6 Words as values
A block that is not given to a combining word is a value: it can be kept in a local, returned, or given to a parameter of type word (signature), and call calls it. Such a block sees no locals around it. To give a word of the program as a value, write do NAME end with no signature: the value is that word, with its signature.
04-word-values.pnx
use io
use str
word up (a int, b int gives bool) a b lt end
word down (a int, b int gives bool) a b gt end
word best (before word (int, int gives bool), x int, y int gives int) y x before call if y else x end end
word order_named (how str gives word (int, int gives bool) or fail)
how case
"up" of do up end end
"down" of do down end end
otherwise err.not_found fail end
end
end
word demo (or fail)
"up" order_named or_fail to smallest_first
"down" order_named or_fail to largest_first
cat ("smallest ", smallest_first 7 3 best, ", largest ", largest_first 7 3 best) io.println
end
demo or_else (e) e err_text io.println end
Output
$ pnx 04-word-values.pnx
smallest 3, largest 7
Q&A
Q. How is a word of the language, such as str.starts_with, given as a value?
A. With the block’s signature written as types only: do (str, str gives bool) str.starts_with end. Inputs written as types stay on the stack, so the word takes them as it is. Without a signature the block takes nothing, and the message shows both forms.
Common mistake: A case branch can hold the word itself: "prefix" of str.starts_with end
str.starts_with is called, and finds too few values on the stack. Make the value with a block: "prefix" of do (str, str gives bool) str.starts_with end end, or do NAME end for a word of the program.04-word-values-branch.pnx
use str
word test_named (how str gives word (str, str gives bool) or fail)
how case
"prefix" of str.starts_with end
otherwise err.not_found fail end
end
end
Refused
$ pnx 04-word-values-branch.pnx
04-word-values-branch.pnx:4:17: the stack does not hold enough values for this word
Q&A
Q. What if the two branches of if should leave values of different types?
A. They cannot. The code after if does not know which branch ran, so both leave the same number of values of the same types. When one branch has a value and the other has not, that is a failure (chapter 8) or a row asked with is_none (chapter 7).
Summary
to NAMEstores into a local,set NAMEinto a global (standard 9.4).- A name made inside a branch is not visible outside it (9.4).
totakes the top value off into a name; using the name puts it back on (9.4).- The two branches of
ifand the branches ofcaseleave the same stack; one turn of a loop leaves the stack it found (10). - A named loop is left at once with
until (NAME)(10.2). - Arrays and lists are walked with stems:
eacheach_indexcountfoldsum… (10.6); the block ofeach,each_indexortimesis left withbreak(10.6 (4)). do NAME endis the wordNAMEas a value; any other word is given asdo (signature) word end(10.5).