6 모음
먼저 알아야 할 것: 4장(줄기와 블록), 5장(버퍼).
이 장이 답하는 물음
- 여러 값을 담는 자리는 어떤 것들이 있고, 크기는 어떻게 정하는가?
- 찬 목록에 넣거나 빈 큐에서 꺼내면 무슨 일이 일어나는가?
- 배열을 거르거나 바꿔 다른 자리에 담으려면?
모든 모음은 선언할 때 용량이 정해지고, 상태의 덩이 안에 그 자리가 미리 잡힌다. 돌면서 자라는 저장소는 없다. 그래서 “얼마나 담을 수 있는가” 는 늘 소스에 적혀 있다.
6.1 배열
array 이름 타입 용량 은 늘 가득 찬 칸의 열이다. 모든 칸이 0 으로 시작하고, len 은 언제나 용량이다. 칸을 읽는 것은 i 배열 idx, 넣는 것은 값 i set 배열 이다(전역에 넣을 때 쓰는 set 과 같다). assign 은 정의 밖에서 앞 칸부터 채운다.
06-array.pnx
use io
use str
const days 7
array rain int days rem always full: 7 cells, all 0 at first
assign rain (0, 5, 12, 0, 3) rem cells 5 and 6 stay 0
8 6 set rain rem value, index, set, array: cell 6 becomes 8
word show () rain do (r int) r str.to_str io.print " " io.print end each "" io.println end
show
cat ("rainy days: ", rain do (r int gives bool) r 0 gt end count, " of ", rain len) io.println
출력
$ pnx 06-array.pnx
0 5 12 0 3 0 8
rainy days: 4 of 7
번호가 범위 밖이면 결함이다. 범위를 미리 알 수 없는 번호는 len 과 견주고 쓴다.
06-index-range.pnx
use io
array week int 7
word day (i int gives int) i week idx end
3 day drop
"day 3 is fine" io.println
7 day drop
결함으로 멈춤
$ pnx 06-index-range.pnx
day 3 is fine
06-index-range.pnx:3: index out of range
i int = 7
6.2 목록
list 는 길이가 0 에서 용량까지 자라는 배열이다. push 로 끝에 넣고 pop 으로 끝에서 뺀다. 번호는 길이 안에서만 쓴다.
06-list.pnx
use io
use str
list stack u8 4 rem grows from 0 to 4
word balanced (s str gives bool) rem are the brackets of s balanced?
stack clear
s str.bytes do (b u8)
b '(' eq if b stack push or_else (e) end end
b ')' eq if stack pop or_else (e) 0 u8 end drop end
end each
stack is_empty
end
word check (s str) cat (s, " ", s balanced) io.println end
"(a(b)c)" check
"(a(b c)" check
출력
$ pnx 06-list.pnx
(a(b)c) true
(a(b c) false
목록을 다루는 낱말을 한 번에 보자. 주석이 각 줄의 일을 적는다.
06-list-steps.pnx
use io
use str
list scores int 3 rem room for 3; it starts empty
word show () cat ("length ", scores len, " of ", scores capacity) io.println end
show rem length 0 of 3
70 scores push or_else (e) end rem push can fail (full), so its guard follows it
85 scores push or_else (e) end
show rem length 2 of 3
cat ("first ", 0 scores idx, ", second ", 1 scores idx) io.println rem i list idx reads cell i
90 1 set scores rem v i set list puts v into cell i
cat ("second is now ", 1 scores idx) io.println
scores pop or_else (e) -1 end str.to_str io.println rem the last one comes off: 90
show rem length 1 of 3
출력
$ pnx 06-list-steps.pnx
length 0 of 3
length 2 of 3
first 70, second 85
second is now 90
90
length 1 of 3
push 와 pop 한 번씩을 스택에서 따라가면 이렇다. 둘 다 실패할 수 있어 지킴 낱말이 바로 뒤에 온다.
| 토큰 | 그 뒤의 스택 | 일어난 일 |
|---|---|---|
70 | 70 | 넣을 값. |
scores push | — | 값을 끝에 넣는다. 찼다면 실패하고 값은 그대로 남지 않는다. |
or_else (e) end | — | 성공했으므로 건너뛴다. 실패였다면 몸이 돌고, 몸이 비었으니 아무것도 남기지 않는다. |
scores pop | 90 | 끝 값을 뺀다. |
or_else (e) -1 end | 90 | 성공: 건너뛴다. 실패였다면 -1 이 그 자리를 채운다. |
str.to_str io.println | — | 찍는다. |
| 쓰는 꼴 | 스택 | 하는 일 |
|---|---|---|
목록 len | — → int | 지금의 길이 |
목록 capacity | — → int | 선언한 용량 |
i 목록 idx | int → 값 | 칸 i 를 읽는다. 길이 밖이면 결함 range |
v i set 목록 | 값 int → — | 칸 i 에 넣는다. 길이 밖이면 결함 range |
v 목록 push | 값 → — | 끝에 넣는다. 차 있으면 실패 full |
목록 pop | — → 값 | 끝에서 뺀다. 비어 있으면 실패 range |
push 와 pop 은 실패할 수 있다. 찬 목록에 넣으면 full 로, 빈 목록에서 빼면 range 로 실패한다. 실패한 push 는 목록을 바꾸지 않는다.
06-list-full.pnx
use io
list two int 2
word fill (or fail) 1 two push or_fail 2 two push or_fail 3 two push or_fail end
fill or_else (e) cat ("the third push failed: ", e err_text) io.println end
cat ("the list holds ", two len) io.println
출력
$ pnx 06-list-full.pnx
the third push failed: full
the list holds 2
실패할 수 있으므로 push 와 pop 뒤에는 늘 지킴 낱말(8장)이 와야 한다. 빠뜨리면 적재 때 거절된다. 실패를 무시하고 싶을 때도 or_else (e) end 라고 적어서, 무시한다는 뜻을 소스에 남긴다.
06-push-unguarded.pnx
list scores int 3
word add_score (n int) n scores push end rem push can fail: what then? the guard is missing
거절
$ pnx 06-push-unguarded.pnx
06-push-unguarded.pnx:2:38: a word that can fail is followed by or_fail, or_return or or_else
문답
물음. 용량을 넉넉히 잡으면 메모리를 버리지 않는가?
답. 버린다. 그러나 그 양이 소스에 보이고, 적재할 때 정해진다. 돌다가 메모리가 모자라 멈추는 일 대신 push 의 실패로 드러나고, 그 실패는 다뤄야만 적재된다.
6.3 큐
queue 는 먼저 넣은 것이 먼저 나온다. put 으로 넣고, take 로 꺼내고, peek 으로 꺼내지 않고 본다. 작업 사이에서 비거나 찬 큐를 기다리는 wait_take·wait_put 은 13장에서 본다.
06-queue.pnx
use io
use str
queue line int 3
word arrive (n int) n line put or_else (e) cat ("turned away: ", n) io.println end end
word serve () line take or_else (e) cat ("nobody waits: ", e err_text) io.println return end to n cat ("served ", n) io.println end
1 arrive 2 arrive 3 arrive 4 arrive
serve serve
5 arrive
serve serve serve
출력
$ pnx 06-queue.pnx
turned away: 4
served 1
served 2
served 3
served 5
nobody waits: range
흔한 오해: 큐도 each 로 훑는다
each 에만 풀이다(7장). 큐, 버퍼, 찾아보기는 줄기가 아니다. 큐의 원소를 차례로 다루려면 take 를 루프에서 부른다.06-queue-stem.pnx
use io
queue q int 4
q do (x int) end each
거절
$ pnx 06-queue-stem.pnx
06-queue-stem.pnx:3:14: only an array, a list or a pool is a stem (not a queue, a buffer or an index)
6.4 거르고 바꿔 담기
map_into 와 filter_into 는 줄기의 원소를 블록으로 바꾸거나 거른 결과를 이미 있는 자리에 담는다. 새 배열이 생기지 않는다. 목적지가 목록이면 끝에 넣고, 배열이면 0 번 칸부터 쓰고 쓴 칸의 수를 준다. 목적지가 차면 실패한다.
06-map-filter.pnx
use io
use str
array prices int 6
assign prices (1200, 450, 3000, 780, 99, 2500)
list cheap int 6
array with_tax int 6
word go (or fail)
prices do (p int gives bool) p 1000 lt end cheap filter_into or_fail
prices do (p int gives int) p 11 mul 10 div end with_tax map_into or_fail to n
cat ("cheap: ", cheap len, " items, the first ", 0 cheap idx) io.println
cat ("with tax, ", n, " cells written, the first ", 0 with_tax idx) io.println
end
go or_else (e) e err_text io.println end
출력
$ pnx 06-map-filter.pnx
cheap: 3 items, the first 450
with tax, 6 cells written, the first 1320
6.5 모음을 낱말에 넘기기
낱말은 모음을 매개변수로 받을 수 있다. 모음의 종류와 칸이 담는 것을 쓰고 — list int, array city, queue int, list str(8) — 또는 buffer 라고 쓰고, 낱말을 부를 때 그 자리에 모음의 이름을 준다. 안에서 매개변수는 모음의 이름이 쓰이는 모든 자리에 쓰인다(map_into 의 목적지까지). 이제 같은 낱말이 그런 종류의 목록 모두에, 용량에 상관없이 쓰인다.
06-coll-param.pnx
use io
list morning int 8
list evening int 8
word record_all (l list int, a int, b int, c int)
a l push or_else (e) end b l push or_else (e) end c l push or_else (e) end
end
word report (name str, l list int)
cat (name, ": ", l len, " readings, highest ", l maximum or_else (e) 0 end) io.println
end
morning 12 15 11 record_all
evening 21 19 20 record_all
"morning" morning report
"evening" evening report
출력
$ pnx 06-coll-param.pnx
morning: 3 readings, highest 15
evening: 3 readings, highest 21
흔한 오해: 매개변수 타입으로 쓴 표 이름은 표를 받는다
매개변수 타입으로 쓴 레코드 표의 이름은 그 표의 한 줄을 뜻한다(7장). 표 전체를 받으려면list 레코드 나 array 레코드 라고 쓴다.06-coll-param-row.pnx
record reading
value i32
end
list morning reading 8
word count_them (l morning gives int) 0 end rem a table's name as a type: one row of it
morning count_them
거절
$ pnx 06-coll-param-row.pnx
06-coll-param-row.pnx:6:9: this parameter takes one row of a table; a parameter that takes the whole table is written list RECORD or array RECORD
문답
물음. 목록을 비우거나 줄이려면?
답. 목록 clear 는 길이를 0 으로, n 목록 truncate 는 길이를 n 으로 줄인다. 칸의 값은 그대로 있지만 길이 밖이라 보이지 않는다. 배열은 늘 가득 차 있으므로 비울 수 없고, 칸마다 set 으로 넣는다.
정리
- 낱말은 모음을
list T,array T,queue T,buffer로 받는다(9.9). - 모음의 용량은 선언이 정한다. 돌면서 자라는 저장소는 없다(표준 8.1).
- 배열은 늘 가득 차 있고, 목록은 길이가 자란다. 범위 밖 번호는 결함이다(8.4, 8.5).
pushpopputtake는 실패할 수 있다: 차면full, 비면range(8.5, 8.6).- 배열과 목록만 줄기다(10.6).
map_intofilter_into는 주어진 목적지에 담는다(10.6).