5 문자열
먼저 알아야 할 것: 2장(값의 타입), 4장(줄기와 블록).
이 장이 답하는 물음
- 문자열을 자르고 가르는데 왜 복사가 일어나지 않는가?
str와str(N)은 무엇이 다르고, 언제 어느 것을 쓰는가?- 새 글을 만드는 바이트는 어디에 놓이고, 어떻게 되돌리는가?
5.1 뷰: 복사하지 않는 문자열
str 값은 뷰(view)다. 바이트가 있는 자리와 길이만 가리키고, 바이트를 갖지 않는다. 그래서 자르기 (str.take str.skip str.slice), 다듬기(str.trim), 가르기(str.cut str.split str.lines str.words)는 바이트를 하나도 복사하지 않고 원래 글의 일부를 가리키는 새 뷰를 준다.
05-views.pnx
use io
use str
rem Parse "key = value" lines without copying a byte.
word parse (line str)
line str.trim to t
t str.is_empty t "#" str.starts_with or if return end rem blank lines and remarks
t "=" str.try_cut or_else (e) cat ("no '=' in: ", t) io.println return end to (key, value)
cat ("[", key str.trim, "] = [", value str.trim, "]") io.println
end
text END
# settings
name = Pinax
version= 1
broken line
END
str.lines do (l str) l parse end each
출력
$ pnx 05-views.pnx
[name] = [Pinax]
[version] = [1]
no '=' in: broken line
뷰가 무엇인지 작은 예로 먼저 보자. 아래의 head, tail, name 은 모두 같은 리터럴의 바이트를 가리킨다. 새 바이트는 cat 이 출력할 글을 만들 때 처음 생긴다.
05-slices.pnx
use io
use str
word show ()
"Hello, Pinax world" to s rem s is a view of the 18 bytes of the literal
s 5 str.take to head rem the first 5 bytes: "Hello", no copy
s 7 str.skip to tail rem all but the first 7: "Pinax world", no copy
tail 0 5 str.slice to name rem bytes 0 up to 5 of tail: "Pinax"
cat ("[", head, "] [", tail, "] [", name, "]") io.println rem cat makes new bytes, in the region
cat ("lengths: ", s str.len, " ", head str.len, " ", name str.len) io.println
end
show
출력
$ pnx 05-slices.pnx
[Hello] [Pinax world] [Pinax]
lengths: 18 5 5
| 뷰 | 가리키는 곳 | 길이 |
|---|---|---|
s | 리터럴의 0 번 바이트부터 | 18 |
head | 같은 자리, 0 번부터 | 5 |
tail | 같은 리터럴의 7 번부터 | 11 |
name | tail 의 0 번, 곧 리터럴의 7 번부터 | 5 |
위의 05-views.pnx 는 설정 글을 줄로 가르고, 줄을 다듬고, = 에서 둘로 자른다. 바이트를 옮기는 일은 출력할 때뿐이다. str.try_cut 은 = 가 없으면 실패하므로 or_else 로 받았다. 실패하지 않는 str.cut 은 = 가 없으면 뒤를 빈 글로 준다.
5.2 저장된 글
뷰는 자기가 가리키는 바이트보다 오래 살 수 없다. 그래서 전역, 배열의 칸, 레코드의 필드처럼 오래 사는 자리에는 뷰를 둘 수 없고, 저장된 글 str(N) 을 쓴다. str(N) 은 N 바이트의 자리를 차지하고, 넣을 때 바이트를 복사한다. 읽으면 그 자리를 가리키는 뷰가 된다.
05-stored.pnx
use io
use str
global longest str(16) rem stored text: 16 bytes, copied in
word keep_longest (w str) w str.len longest str.len gt if w set longest end end
"the quick brown fox jumps over the lazy dog" str.words do (w str) w keep_longest end each
cat ("longest word: ", longest) io.println
출력
$ pnx 05-stored.pnx
longest word: quick
05-global-view.pnx
global name str
거절
$ pnx 05-global-view.pnx
05-global-view.pnx:2:1: stored text is str(N); a str view lives only in locals, arguments and results
어느 것을 쓸지는 그 글이 얼마나 오래 사는가로 고른다.
str(뷰) | str(N)(저장된 글) | |
|---|---|---|
| 쓰는 자리 | 지역 변수, 인자, 결과 | 전역, 배열과 목록의 칸, 레코드의 필드 |
| 바이트 | 갖지 않는다. 다른 곳의 바이트를 가리킨다 | N 바이트의 자리를 갖는다 |
| 넣을 때 | 복사하지 않는다 | 복사한다. N 보다 길면 결함, 지킴 낱말이 있으면 실패 range |
| 사는 동안 | 가리키는 바이트가 사는 동안(실행 하나, 또는 release 까지) | 그 자리가 사는 동안 |
필드에 뷰를 두려고 하면 적재 때 거절되고, 메시지가 str(N) 을 쓰라고 알려 준다.
05-field-view.pnx
record person
name str rem a view cannot be kept in a field: write str(N)
end
거절
$ pnx 05-field-view.pnx
05-field-view.pnx:3:1: a field holds str(N); a str view lives only in locals, arguments and results
흔한 오해: str(N) 보다 긴 글은 잘려서 들어간다
str.take 로 먼저 자른다.05-too-long.pnx
use io
global name str(8)
"Pinax" set name
cat ("stored: ", name) io.println
"an embeddable language" set name
결함으로 멈춤
$ pnx 05-too-long.pnx
stored: Pinax
05-too-long.pnx:5: index out of range
문답
물음. 글이 밖에서 와서 너무 길 수 있다. 길이를 먼저 비교해야 하는가?
답. 아니다. 실패할 수 있는 다른 낱말처럼, 넣는 일 뒤에 지킴 낱말을 붙인다. 그러면 너무 긴 글은 실패 range 이고, 자리는 가졌던 것을 그대로 가진다. 지킴 낱말이 없으면 여전히 결함이다.
05-too-long-guard.pnx
use io
global name str(8)
word remember (v str)
v set name or_else (e) cat ("too long, kept ", name) io.println return end
cat ("stored: ", name) io.println
end
"Pinax" remember
"an embeddable language" remember
출력
$ pnx 05-too-long-guard.pnx
stored: Pinax
too long, kept Pinax
지킴 낱말은 넣는 일 뒤에 오므로, 낱말이 그 앞에 한 일은 그대로 남는다. 다른 일(줄을 받기, 수를 올리기)보다 먼저 글을 거절하려면, 먼저 fits 로 자리에 묻는다. v fits entry.name 은 글을 가져가고, 같은 실패 range 로 실패하며, 아무것도 쓰지 않는다. 너비는 자리의 것이다.
05-fits.pnx
use io
record entry
name str(8)
end
list names entry 4
word enter (v str)
v fits entry.name or_else (e) cat (v, ": too long, no row taken") io.println return end
names add_row or_else (e) "full" io.println return end to r
v r to entry.name
cat ("added ", r entry.name, ", rows ", names len) io.println
end
"Pinax" enter
"an embeddable language" enter
출력
$ pnx 05-fits.pnx
added Pinax, rows 1
an embeddable language: too long, no row taken
5.3 새 글 만들기와 버퍼
cat 과 str.upper str.repeat 처럼 새 바이트를 만드는 낱말은 그 바이트를 구역에 쓰고 뷰를 준다. 글을 조금씩 이어 붙일 때는 버퍼를 선언하고 append 류의 낱말로 붙인다. 버퍼가 차면 붙이는 낱말이 실패한다.
05-build.pnx
use io
use str
use fmt
buffer out 64
word report (or fail)
"id: " out append or_fail
42 out append_int or_fail
", hex: 0x" out append or_fail
255 out fmt.append_hex or_fail
'!' out append_byte or_fail
end
report or_else (e) cat ("buffer full: ", e err_text) io.println end
out contents io.println
cat ("upper: ", "pinax" str.upper, ", repeat: ", "ab" 3 str.repeat, ", padded: [", "7" 4 str.pad_left, "]") io.println
출력
$ pnx 05-build.pnx
id: 42, hex: 0xff!
upper: PINAX, repeat: ababab, padded: [ 7]
5.4 구역을 되돌리기
구역은 실행마다 정해진 크기다. 루프 안에서 글을 계속 만들면 구역이 찬다. mark 로 지금 자리를 기억하고 release 로 되돌리면 그 사이에 만든 바이트를 다시 쓴다.
05-region.pnx
use io
use str
word line (i int gives str) cat ("line ", i) end
word lots ()
region_free to before
1 10001 range do (i int)
mark to m rem every turn gives its bytes back
i line drop
m release
end each
cat ("bytes still taken after 10000 lines: ", before region_free sub) io.println
end
lots
출력
$ pnx 05-region.pnx
bytes still taken after 10000 lines: 0
되돌린 뒤에는 그 사이에 만든 뷰를 쓸 수 없다. 처리기는 이것도 적재 때 검사한다.
05-release-view.pnx
use io
word f ()
mark to m
cat ("made ", "after the mark") to s
m release
s io.println
end
거절
$ pnx 05-release-view.pnx
05-release-view.pnx:6:3: this str was made after a mark that was released since; the region may hold other bytes there now
문답
물음. 되돌린 뒤에도 남겨야 할 글은 어떻게 하는가?
답. str(N) 전역이나 칸에 넣는다. 넣을 때 복사되므로 구역을 되돌려도 남는다. 위의 05-stored.pnx 가 그렇게 한다.
문답
물음. defer m release 아래에서 each 안의 낱말이 실패한다. or_fail 로 실패를 넘길 수 있는가?
답. 그렇다. 실패하는 낱말이 나가는 길에서 release 는 스택에 남은 글을 보지 않는다. 낱말은 거기서 끝나고 아무도 그것을 다시 읽지 않는다. stem 은 첫 실패에서 멈춘다.
05-release-failing.pnx
use io
use str
word short (w str or fail)
w str.len 5 gt if err.range fail end
end
word read_words (line str or fail)
mark to m
defer m release end
line str.upper str.words do (w str) w short or_fail w io.println end each
end
"one two three" read_words or_else (e) cat ("stopped: ", e err_text) io.println end
"one embeddable two" read_words or_else (e) cat ("stopped: ", e err_text) io.println end
출력
$ pnx 05-release-failing.pnx
ONE
TWO
THREE
ONE
stopped: range
5.5 바이트와 글자
길이와 자리는 모두 바이트로 센다. 한글 한 글자는 UTF-8 로 3 바이트다. 글자 단위가 필요하면 str.char_count 와 str.chars 를 쓴다. str.chars 는 글자마다 그 글자의 뷰를 낸다(코드포인트 수가 아니다).
05-chars.pnx
use io
use str
word show ()
"한글 ok" to s
cat ("bytes ", s str.len, ", characters ", s str.char_count) io.println
s str.chars do (c str) cat ("<", c, ">") io.print end each
"" io.println
end
show
출력
$ pnx 05-chars.pnx
bytes 9, characters 5
<한><글>< ><o><k>
5.6 글자 부류와 수 읽기
글의 앞에서 “숫자가 이어지는 만큼” 을 떼려면 str.span 을 쓴다. 부류는 상수 str.digit str.alpha str.alnum str.space str.upper_case str.lower_case str.hex_digit str.punct 다(모두 ASCII). 소수점이 있는 수는 str.to_dec 가 dec(N) 으로 읽는다(2장). 점 아래 자리가 N 보다 많으면 실패다.
05-classes.pnx
use io
use str
rem span cuts off the run of one class of bytes at the front; to_dec reads a decimal.
word demo ()
"42abc" str.digit str.span to (digits, rest)
cat ("[", digits, "] [", rest, "]") io.println
" x" str.space str.span to (blanks, after)
cat (blanks str.len, " blanks, then ", after) io.println
"12.50" 2 str.to_dec or_else (e) 0.00 end to price
cat ("price ", price) io.println
"12.505" 2 str.to_dec or_else (e) cat ("not a dec(2): ", e err_text) io.println 0.00 end drop
end
demo
출력
$ pnx 05-classes.pnx
[42] [abc]
3 blanks, then x
price 12.50
not a dec(2): format
| 토큰 | 그 뒤의 스택 | 일어난 일 |
|---|---|---|
"42abc" str.digit | "42abc" 48 | 글과 부류(부류는 작은 정수다). |
str.span | "42" "abc" | 앞의 숫자 부분과 나머지. 둘 다 같은 글을 가리키는 뷰다. |
to (digits, rest) | — | 오른쪽 이름이 맨 위 값을 받는다. |
조각을 이어 붙이거나 바꾼 글을 버퍼에 바로 쓰는 낱말도 있다. str.join_into 는 줄기의 조각을 사이 글로 이어 버퍼에 붙이고, str.replace_into 는 바꾼 글을 버퍼에 붙이며 바꾼 횟수를 준다. 둘 다 버퍼가 차면 실패한다. join_into 는 줄기를 맨 뒤에 둔다 — 줄기는 바로 뒤의 낱말이 받기 때문이다(4장).
05-join.pnx
use io
use str
buffer joined 64
buffer fixed 64
word demo (or fail)
", " joined "alpha beta gamma" str.words str.join_into or_fail rem the stem comes last
joined contents io.println
"hello world" "o" "0" fixed str.replace_into or_fail to n
cat (n, " replaced: ", fixed contents) io.println
end
demo or_else (e) e err_text io.println end
출력
$ pnx 05-join.pnx
alpha, beta, gamma
2 replaced: hell0 w0rld
문답
물음. 글자 부류는 한글이나 다른 유니코드 글자도 아는가?
답. 모른다. 부류는 ASCII 범위만 본다. 한글 한 글자는 str.alpha 에 들지 않는다. 바이트가 아닌 글자 단위가 필요하면 str.chars 로 글자를 하나씩 받아 직접 견준다.
정리
str는 뷰다. 자르고 가르는 낱말은 복사하지 않는다(표준 7.3, 16.1).- 오래 사는 자리(전역, 칸, 필드)에는 뷰를 둘 수 없다(7.3).
- 오래 사는 자리에는
str(N)을 쓴다. 넣을 때 복사되고, 길면 결함이다. 넣는 일 뒤에 지킴 낱말이 있으면 실패range다(7.3). - 새 바이트는 구역에 놓이고,
mark/release로 되돌린다. 되돌린 뒤의 뷰는 거절된다(12.2, 12.3). - 버퍼는 글을 이어 붙이는 자리다. 차면 실패한다(8.7).
- 길이는 바이트다. 글자는
char_count,chars(16.1).