11 글과 바이트의 형식

먼저 알아야 할 것: 5장(뷰, 버퍼), 7장(레코드), 8장(실패).

이 장이 답하는 물음

Pinax 에는 힙이 없으므로 JSON 을 객체의 나무로 읽지 않는다. 대신 한 방향으로 읽는다: 토큰 하나, 필드 하나씩. 쓸 때는 버퍼에 바로 쓴다. 레코드와 맞추는 일은 레코드의 선언에서 만들어진 코드가 한다.

11.1 CSV 와 JSON

csv.unpack 은 한 줄을 읽어 레코드의 필드에 차례로 넣고, json.pack 은 레코드를 필드 이름이 키인 객체로 쓴다.

11-csv-to-json.pnx

use io
use csv
use json
use str
rem Read CSV rows into records, write them out as JSON.
record city
  name str(16)
  people i32
  capital bool
end
list cities city 8
buffer out 512
word read_rows (input str or fail)
  input str.lines do (line str)
    cities len to before
    cities add_row or_fail to c
    line c csv.unpack or_else (e) before cities truncate  cat ("bad row: ", line) io.println  "" end drop
  end each
end
word to_json (or fail)
  out json.begin_array or_fail
  cities do (c cities) c out json.pack or_else (e) end end each
  out json.end_array or_fail
end
text END
Seoul,9411000,true
Busan,3349000,false
Daejeon,lots,false
END
read_rows or_else (e) end
to_json or_else (e) e err_text io.println end
out contents io.println

출력

$ pnx 11-csv-to-json.pnx
bad row: Daejeon,lots,false
[{"name":"Seoul","people":9411000,"capital":true},{"name":"Busan","people":3349000,"capital":false}]

줄을 먼저 더하고 unpack 이 실패하면 before cities truncate 로 그 줄을 되돌린다. truncate 는 목록의 길이를 더하기 전의 길이로 줄인다.

흔한 오해: 레코드 목록에서도 pop 으로 마지막 줄을 뺀다

레코드의 목록에는 pop 이 없다(값의 목록에만 있다). 마지막 줄은 truncate 로 거둔다. 다만 그 목록의 줄을 찾아보기나 필드가 담고 있으면 truncate 도 거절된다. 그때는 줄을 더하기 전에 확인한다.

11-pop-records.pnx

record city
  name str(16)
end
list cities city 4
word undo () cities pop or_else (e) cities none end drop end

거절

$ pnx 11-pop-records.pnx
11-pop-records.pnx:5:21: pop takes from a list of values; a list of records has no pop

JSON 을 토큰으로 읽으면 깊이와 상관없이 필요한 것만 고를 수 있다. json.next 는 토큰의 갈래, 값의 뷰, 나머지를 준다.

11-json-read.pnx

use io
use json
use str
rem Sum every number in a JSON text, at any depth, without building a tree.
word sum_numbers (s str gives int)
  0 to total
  s to rest
  loop
    rest json.next or_else (e) break end to (kind, value, rest)
    kind json.number eq if total value str.to_int or_else (e) 0 end add to total end
  end
  total
end
text END
{"a": 1, "b": [2, 3, {"c": 4}], "d": {"e": [5]}, "f": "6"}
END
sum_numbers str.to_str io.println

출력

$ pnx 11-json-read.pnx
15

json.next 가 주는 셋을 한 토큰씩 보자. s json.next 는 (kind int, value str, rest str) 를 준다: 토큰의 갈래, 그 토큰의 글(뷰), 그리고 그 뒤에 남은 글이다. 남은 글을 다시 json.next 에 주는 것을 되풀이하면 글 전체를 앞에서부터 한 번 훑는다. 글이 끝나면 json.next 가 실패하고, 루프는 그 실패로 끝난다.

11-json-tokens.pnx

use io
use json
rem The tokens of a JSON text, one at a time: what json.next gives for each.
word kind_name (k int gives str)       rem module constants are compared with eq, not case
  k json.object eq if "object" return end
  k json.object_end eq if "object end" return end
  k json.array eq if "array" return end
  k json.array_end eq if "array end" return end
  k json.key eq if "key" return end
  k json.string eq if "string" return end
  k json.number eq if "number" return end
  "other"
end
word show_tokens (s str)
  s to rest
  loop
    rest json.next or_else (e) break end to (kind, value, rest)   rem fails at the end: leave the loop
    cat (kind kind_name, " [", value, "]") io.println
  end
end
"{\"name\": \"Pinax\", \"sizes\": [8, 16]}" show_tokens

출력

$ pnx 11-json-tokens.pnx
object [{]
key [name]
string [Pinax]
key [sizes]
array [[]
number [8]
number [16]
array end []]
object end [}]
토큰그 뒤의 스택일어난 일
rest json.nextjson.object "{" …{ 를 읽었다. 갈래 json.object, 남은 글은 "name": ….
or_else (e) break endjson.object "{" …실패하지 않았으므로 몸을 건너뛴다.
to (kind, value, rest)—셋을 이름에 담는다. 오른쪽 rest 가 맨 위 값을 받는다.

갈래는 json.object 처럼 모듈의 상수이므로 case 가 아니라 eq 로 견준다(4장). 갈래마다 할 일만 고르면 되므로, 깊이가 얼마든 필요한 값만 뽑을 수 있다. 11-json-read.pnx 는 json.number 만 골라 더했다.

관심 없는 값은 json.skip 으로 한 번에 건너뛴다. 객체나 배열이면 그 안 전부를 건너뛰므로 깊이를 셀 필요가 없다. 갈래의 상수는 json.object json.object_end json.array json.array_end json.key json.string json.number json.boolean json.null 이다. 거꾸로 객체 하나를 레코드의 줄에 채우는 것은 json.unpack 이다: 모든 필드가 맞을 때만 줄에 쓰고, 아니면 실패다.

11-json-skip.pnx

use io
use str
use json
record point
  x int
  y int
end
array pts point 1
word demo (or fail)
  "{\"tags\": [1, [2, 3]], \"x\": 4}" json.next or_fail to (k, v, rest)   rem the {
  rest json.next or_fail to (k, v, rest)                                   rem the key tags
  rest json.skip or_fail to rest                                           rem the whole array, however deep
  rest json.next or_fail to (k, v, rest)                                   rem the key x
  rest json.next or_fail to (k, v, rest)                                   rem 4
  cat ("after the skip: ", v, ", a number? ", k json.number eq) io.println
  "{\"x\": 7, \"y\": 9}" 0 pts idx json.unpack or_fail drop
  cat ("unpacked ", 0 pts idx point.x, " ", 0 pts idx point.y) io.println
  "{\"x\": true}" 0 pts idx json.unpack or_else (e) cat ("unpack failed: ", e err_text) io.println "" end drop
end
demo or_else (e) e err_text io.println end

출력

$ pnx 11-json-skip.pnx
after the skip: 4, a number? true
unpacked 7 9
unpack failed: format

CSV 를 쓰는 쪽은 필드 하나씩이다. csv.put 은 글을(쉼표, 따옴표, 줄바꿈이 들어 있으면 따옴표로 싸서), csv.put_int csv.put_dec csv.put_bool 은 수와 참거짓을, csv.end_line 은 줄 끝을 버퍼에 붙인다. 앞에 필요한 쉼표는 낱말이 넣는다. 레코드의 줄 하나를 한 줄로 쓰는 것은 csv.pack 이다.

11-csv-write.pnx

use io
use csv
buffer out 64
word demo (or fail)
  "kim" out csv.put or_fail  3 out csv.put_int or_fail  true out csv.put_bool or_fail  out csv.end_line or_fail
  "a, b" out csv.put or_fail  2.50 out csv.put_dec or_fail  out csv.end_line or_fail
  out contents io.print
end
demo or_else (e) e err_text io.println end

출력

$ pnx 11-csv-write.pnx
kim,3,true
"a, b",2.50

11.2 바이트

모듈 bytes 는 정해진 폭과 바이트 순서로 쓰고 읽는다. 레코드에 big_endian 을 적으면 bytes.pack 이 큰 끝부터 쓴다. varint 필드는 작은 수를 적은 바이트로 쓴다.

11-binary.pnx

use io
use bytes
use encode
use hash
use fmt
record header
  big_endian
  magic u32
  version u16
  length varint
end
array headers header 2
buffer wire 32
buffer hex 64
word demo (or fail)
  0 headers idx to h
  0x50494e58 u32 h to header.magic           rem "PINX"
  1 u16 h to header.version
  300 h to header.length
  h wire bytes.pack or_fail
  wire contents hex encode.hex_encode or_fail
  cat ("bytes: ", hex contents, ", crc32: ", wire contents hash.crc32 fmt.hex) io.println
  1 headers idx to back
  wire contents back bytes.unpack or_fail drop
  cat ("read back: version ", back header.version, ", length ", back header.length) io.println
end
demo or_else (e) e err_text io.println end

출력

$ pnx 11-binary.pnx
bytes: 50494e580001ac02, crc32: 31d80bed
read back: version 1, length 300

쓰는 낱말(pack, put_…, encode.…_encode)은 버퍼가 차면 실패한다. 그래서 뒤에 늘 지킴 낱말이 와야 한다.

11-pack-unguarded.pnx

use json
record point
  x int
  y int
end
list points point 2
buffer out 64
word write (p points) p out json.pack end   rem pack fails when the buffer is full: no guard

거절

$ pnx 11-pack-unguarded.pnx
11-pack-unguarded.pnx:8:39: a word that can fail is followed by or_fail, or_return or or_else

문답

물음. 메모리 안의 레코드도 큰 끝으로 놓이는가?

답. 아니다. big_endian 은 pack 과 unpack 이 쓰는 바이트의 차례만 정한다. 메모리 안의 모양은 처리기가 정하고, 스크립트는 그것을 볼 수 없다.

11.3 그 밖의 바꾸기

fmt 는 수를 십육진, 이진, 0 을 채운 모양으로 바꾼다. hash 는 CRC-32, FNV-1a, SipHash 를, encode 는 Base64, 십육진, URL 퍼센트 부호화를 준다. 바꾼 결과를 버퍼에 쓰는 낱말은 버퍼가 차면 실패한다. 자세한 목록은 표준 16절에 있다.

해시는 글을 받아 int 를 준다. hash.crc32 는 검사합으로, hash.fnv1a 와 hash.hash64 는 키를 섞는 데 쓴다. 레코드 한 줄이 bytes.pack 으로 몇 바이트가 되는지는 bytes.packed_size 가 미리 알려 준다 — 버퍼의 자리를 가늠할 때 쓴다.

11-hash.pnx

use io
use fmt
use hash
use bytes
record pair
  a u16
  b int
end
array pairs pair 1
word demo ()
  cat ("crc32 ", "pinax" hash.crc32 fmt.hex, ", fnv1a ", "pinax" hash.fnv1a fmt.hex) io.println
  cat ("a pair packs into ", 0 pairs idx bytes.packed_size, " bytes") io.println
end
demo

출력

$ pnx 11-hash.pnx
crc32 19afea99, fnv1a 131b946615bc7b35
a pair packs into 10 bytes

문답

물음. JSON 으로 쓸 때 들여쓰기나 줄바꿈을 넣을 수 있는가?

답. 없다. json 의 쓰는 낱말은 한 줄로 붙인다. 사람이 읽을 모양이 필요하면 받은 쪽에서 다듬는다. 읽는 쪽은 공백과 줄바꿈이 있어도 그대로 읽는다.

정리