본문으로 건너뛰기

setmap

설계 문서 목록으로

STRUCT DSL의 컨테이너 타입입니다. 형식은 움직이지 않습니다set은 배열 컬럼 하나이고 map은 키·값 컬럼 둘입니다. 새로 드는 것은 표기와 검사, 그리고 모든 언어의 컨테이너 타입 입니다.


1. 지금까지 된 것

STRUCT DSL 설계 §4.7이 「문법과 모델까지 받고 그 뒤에서 이름과 함께 거절한다」고 정하였고, 그대로 되어 있습니다.

무엇어디상태
렉서의 < · > · ,SchemaLexer.cs받습니다
set<T> · map<K,V> 파싱SchemaParser.cs:667SchemaTypeForm.Container로 읽습니다
타입 표현의 재구성SchemaSyntax.cs:236ToString()이 원래 표기를 되돌립니다
거절SchemaDeclarations.cs:482schema.container-not-supported
컬럼 해석SchemaFieldTypes.cs:112false를 돌려줍니다

그래서 이 문서가 정할 것은 문법의 나머지와 그 뒤 전부입니다.


2. 표기

2.1 타입 표현

field tags set<string>
field prices map<int,int>
field drops map<int,Reward>[]
field owners set<int>?
무엇되는가
배열과의 조합 — set<int>[] · map<int,Reward>[]1차에서 거부합니다. 파일은 담을 수 있는데 셀 표기가 없습니다 — set의 배열은 컬럼 하나가 목록의 목록이 되고, map의 배열은 번호 붙은 그룹(Prices1.Key)이 되는데 그 표기가 정해져 있지 않습니다
값 옵셔널 — set<int>? · map<K,V>?됩니다. 배열의 T[]?와 같은 뜻입니다
원소 옵셔널 — map<K,V?>담지 않습니다(§10)
중첩 컨테이너 — map<int,set<int>>담지 않습니다(§10)
인자 개수가 안 맞는 것 — map<int> · set<int,int>schema.container-arity로 거부합니다. 메시지 id는 이미 있습니다

꺾쇠를 고른 이유는 설계 §4.7에 있습니다 — 대괄호는 배열 표기와 구분되지 않고, 이 DSL에는 비교 연산자가 없어 < >가 비어 있습니다.

2.2 메타데이터의 자리 — 타입마다의 괄호

타입 인자에 괄호를 답니다.

field prices map<int(min=1), int(min=0,max=9999)>(size=..3)
field tags set<string(regex="^[a-z]+$")>(size=1..)
괄호무엇에 대한 것인가들어갈 수 있는 키
타입 인자의 괄호map<int(…),V(…)>그 자리의 값 하나하나min · max · allowed · notDefault · regex · text · asset
필드 뒤의 괄호…>(…)컨테이너 자신size · removed · tag

이 표기가 필요한 이유는 map이 원소 자리를 둘 가지기 때문입니다. 설계 §6.2는 「키 이름 하나가 붙는 자리 하나에 대응한다」로 배열의 원소와 배열 자신을 갈랐는데(int[] (min=1, size=3)), map에는 그 방법이 없습니다 — min이 키의 것인지 값의 것인지 이름으로 갈리지 않습니다. 자리를 이름으로 가를 수 없으면 위치로 갑니다.

배열의 원소에는 안쪽 괄호를 허용하지 않습니다. int(min=1)[]은 거부하고 int[] (min=1)을 안내합니다. 두 표기가 같은 것을 뜻하게 두면 같은 것을 적는 방법이 둘이 되고, 그것은 이 DSL이 /** */를 두지 않은 것과 같은 이유로 하지 않습니다.

컨테이너는 반대입니다 — 인자 자리의 제약은 언제나 안쪽에 적습니다. set은 자리가 하나여서 필드 뒤에 적어도 갈리지만, map과 다르게 적히면 「컨테이너의 제약은 어디에 적는가」에 답이 둘이 됩니다. 필드 뒤 괄호에 원소 자리 키를 적으면 schema.container-element-meta-outside로 보고하고 안쪽을 안내합니다.

size길이 선언이 아니라 검사입니다 — v107이 고정 길이 배열을 형식에서 없앴으므로 길이를 선언하는 표기는 아예 없습니다.

2.3 쓸 수 있는 자리

어디1차근거
.tbs의 struct 멤버 타입됩니다문법이 이미 여기 있습니다
시트의 타입 칸map<int,int>됩니다아래
시트의 타입 칸set<string>거부합니다아래

시트의 타입 칸 — 미룬 이유가 없어진 자리와 남은 자리

미룬 이유는 「타입 문법이 두 곳에 놓인다」였고, 그것은 피할 수 있었습니다. 타입 칸의 꺾쇠를 ParseValueType에 가르치는 대신, 그 셀을 이 표기의 파서에 그대로 넘깁니다(SchemaParser.ParseTypeExpression). 문법은 한 곳에 남고, 셀을 읽는 쪽이 아는 것은 「꺾쇠가 있으면 저쪽을 본다」뿐입니다.

map은 됩니다. 타입 칸의 map<int,int>.Key·.Value 두 컬럼이 되고, 셀은 (나) 표기의 쌍입니다 — 이미 있는 기계를 그대로 씁니다.

set은 거부합니다. 이유가 문법이 아니라 표면이 앉을 자리입니다.

타입 칸의 map타입 칸의 set
컬럼하나
모델레코드 그룹이 됩니다배열 그룹 그대로입니다
조회가 앉을 타입그 레코드의 원소 타입없습니다

조회가 앉을 타입이 없으면 나오는 것은 배열과, 물을 것이 없는 상태입니다. 그것이 정확히 SupportsContainers가 막으려는 반쪽짜리 표면이고, 여기서는 배우지 않은 타깃이 아니라 답을 놓을 자리가 없는 표기 쪽에서 같은 것을 만납니다. 그래서 이름을 대고 거절하며, .tbs에 멤버로 선언하거나 string[]으로 두라고 안내합니다.

되돌릴 수 있는 결정입니다. 조회를 행 클래스에 다는 자리(record.ContainsTags(v))는 모든 언어가 표현할 수 있고, 다중 대상 참조의 접근자가 이미 그 자리를 씁니다. 다만 그것은 언어 16개를 한 번 더 도는 일이고, 그 값을 지금 지불할 이유가 약합니다 — .tbs 한 줄이 같은 것을 냅니다.


3. 모델 — 새 형태 없음

표기모델
set<T>SerialFieldKind = Scalar · IsArray. T[]와 같은 것입니다
map<K,V>SerialFieldKind = Record, 멤버 둘 — KeyValue. 멤버가 2개인 레코드 배열과 같은 것입니다

map<int,Reward>처럼 값이 struct면 Value가 잎이 아니라 그룹이 되고, 그것은 다중 중첩이 이미 지원하는 형태입니다.

새로 드는 것은 표시 하나입니다. SerialField에 「이 필드가 컨테이너로 선언되었다」를 적는 자리가 필요합니다 — 그것이 없으면 생성기가 set<int>int[]를, map<int,int>와 멤버 둘짜리 레코드 배열을 구분할 수 없습니다.

public enum ContainerKind { None, Set, Map }
딸려오는 것내용
Key · Value는 예약된 멤버 이름이 됩니다map이 만든 그룹 안에서만입니다. 사용자가 적은 struct의 멤버 이름과는 겹칠 자리가 없습니다 — 그 struct는 Value 아래에 놓입니다
컬럼 이름Prices.Key · Prices.Value. 값이 struct면 Prices.Value.ItemId처럼 한 겹 더 내려갑니다
컨테이너 아래의 멤버는 저마다 배열이 됩니다map<int,Reward>Reward.itemIdint로 선언되어 있고 컬럼은 int[]입니다 — 그 컬럼 하나가 모든 항목의 그 멤버 값을 담기 때문입니다. 선언에는 이것을 적는 자리가 없고, 컬럼을 만드는 쪽이 압니다

담긴 struct의 멤버가 이미 목록이면 거부합니다. 위의 규칙이 멤버마다 대괄호를 한 겹 씌우므로, 원래 배열이던 멤버는 목록의 목록을 담는 컬럼이 됩니다 — set<T>[]{:을} 거부하는 것과 같은 형태이고, 아래로 끝까지 내려가며 봅니다.


4. 와이어 — 변경 없음

중첩 필드 §4가 이미 확인한 「API는 AoS, 파일은 SoA」가 그대로 적용됩니다. .tcb 버전도 리더도 움직이지 않습니다.

무엇파일에 실리는 형태
set<T>KindArray 컬럼 하나
map<K,V>KindArray 컬럼 둘. 로우마다 길이가 같습니다

파일에 실리는 순서는 시트에 적힌 순서 그대로입니다. 정렬하지 않습니다 — 언어마다 컨테이너의 순회 순서가 다르므로 도구가 정렬하면 어느 언어의 순서를 고를지가 임의가 되고, 골든이 그 임의값을 고정합니다.


5. 셀에 적는 법

5.1 set

배열과 같습니다. a;b;c — 구분자는 DefaultDelimiter입니다.

5.2 map — 두 가지

시트언제
(가) 컬럼 둘Prices.Key1;2;3, Prices.Value10;20;30언제나. 값이 struct인 map은 이것만 됩니다
(나) 한 셀Prices1:10;2:20;3:30값이 스칼라일 때

(나)의 구분자는 두 겹입니다 — 항목 사이가 DefaultDelimiter, 키와 값 사이가 : 입니다. :는 이 도구가 이미 쓰는 기호이므로 어휘가 늘지 않습니다.

경우판정
값에 :가 들어가야 할 때\:로 적습니다. 빈 칸과 없음\-와 같은 백슬래시 규칙입니다
(가)에서 KeyValue의 길이가 다를 때오류입니다. schema.map-length-mismatch로 두 셀을 함께 가리킵니다
(나)에서 항목에 :가 없을 때오류입니다. schema.map-pair-malformed
값이 struct인데 (나)로 적었을 때거부합니다. 성분 구분자가 한 겹 더 필요하고, 그것은 sep이 푸는 문제입니다. 1차에서 두 문제를 함께 풀지 않습니다
(나)를 쓰는 테이블이 와이어 태그를 직접 적을 때거부합니다. 컬럼 하나가 둘이 되므로 태그가 둘 필요한데 시트는 하나를 적었습니다 — sep이 같은 이유로 같은 거부를 합니다

(나)는 (가)로 접힌 다음 아무것도 달라지지 않습니다. 한 셀이 두 컬럼의 텍스트가 되고, 그 뒤로는 보통의 구분자 배열 컬럼입니다 — 원소를 읽고 보고하고 제약을 거는 코드가 이미 있는 그것 그대로입니다.


6. 검사

6.1 키 타입

정확한 동등성이 값 자체에 있는 타입만 키가 됩니다.

타입키로근거
int · bigint · string · enum · uuid · bool됩니다동등성이 표현과 무관합니다
float · double거부합니다동등성이 표현에 달립니다. 같은 수를 두 가지로 적으면 키가 둘이 됩니다
datetime · timespan거부합니다시트의 값이 시간대 해석을 거쳐 오므로, 셀에 같게 적힌 둘이 다른 키가 될 수 있습니다
bitset거부합니다파일에서는 64비트 정수이지만 표기가 플래그 이름의 나열이고, 같은 집합을 여러 순서로 적을 수 있습니다
struct거부합니다멤버마다 컬럼이므로 키가 컬럼 여럿이 되고, 그러면 map이 아니라 uniqueBy의 문제입니다
foreign1차에서 거부합니다(§10)참조의 키 타입이 이 패스보다 뒤에서 정해집니다 — CookingContext.DeferredType

메시지는 schema.map-key-type-not-allowed이고, 거부한 타입과 함께 쓸 수 있는 것을 나열합니다.

6.2 중복

set의 원소 중복과 map의 키 중복은 오류입니다.

Duplicate key `2` in map column `Prices` of table `Shop`.
Sheet `Shop` cell D7, element 3. The first was element 1.

어느 셀인지와 몇 번째 원소인지를 함께 보고합니다. 「어느 셀인지 함께」는 컬럼 제약이 세운 기준이고 여기서도 같습니다.

무엇메시지 id
set 원소 중복schema.set-duplicate-element
map 키 중복schema.map-duplicate-key

비교는 변환된 값으로 합니다. 셀의 문자열이 아니라 파싱한 결과입니다 — 그렇지 않으면 011이 다른 키가 되고, 그것은 파일에 실리는 것과 다른 판정입니다.

6.3 그 밖

무엇판정
빈 셀배열과 같습니다. 원소 0개인 컨테이너입니다
size 검사원소 개수를 셉니다. 범위 표기(n..m · n.. · ..m)는 배열의 것과 같습니다
인자 자리의 제약min · max · allowed 등은 원소마다 적용합니다. 키 자리와 값 자리에 각각입니다

7. 생성 코드 — 순서를 잃지 않는 것이 조건

비용의 실제 위치가 여기입니다. 형식이 안 움직이므로 남는 것은 모든 언어에 컨테이너 타입과 그 구축 코드를 내는 일이고, 중첩 필드 확산과 같은 형태의 작업입니다.

7.1 두 겹의 표면

해시 컨테이너 하나만 내면 순회 순서가 언어마다 달라집니다. 파일의 순서를 정렬하지 않기로 했으므로(§4) 순서는 데이터의 일부이고, 적합성 드라이버는 값을 되읽어 비교하므로 순서가 결과에 들어가는 순간 언어마다 다른 답이 나옵니다.

그래서 모든 언어가 둘 다 냅니다.

무엇set<T>map<K,V>
순서 있는 것TagsT의 배열 하나Prices.Key · Prices.Value — 배열 둘, 파일에 실린 순서 그대로
조회ContainsTags(v)TryGetIndex(k, out at), 그리고 값이 스칼라일 때 TryGetValue(k, out v)

언어에 컨테이너가 있으면 조회를 그것으로 냅니다. 없으면 배열 위의 탐색으로 냅니다 — 다중 대상 참조가 겪은 것과 같은 갈래이고, 호출하는 쪽의 코드는 그대로입니다.

이름의 정확한 철자는 언어마다 AccessorName의 규칙을 따릅니다.

언어마다 조회가 놓이는 자리는 그 언어의 관례를 따릅니다. C#은 레코드 타입의 메서드이고, TypeScript는 인터페이스의 프로퍼티입니다 — 인터페이스는 메서드를 선언하지 않으므로, 다른 언어가 원소 타입에 다는 것을 이 언어는 컨테이너 자체로 답니다. 다중 대상 참조의 접근자가 같은 이유로 같은 갈래를 겪었습니다.

조회가 자리를 돌려주는 이유. 값이 struct인 map은 멤버마다 컬럼 하나이고, 항목 하나에 해당하는 객체가 없습니다 — 돌려줄 것이 없으므로 몇 번째 항목인가를 돌려주고, Value.ItemId[at]으로 읽습니다. 값이 스칼라일 때는 그 위에 TryGetValue를 하나 더 얹습니다. 자리를 돌려주는 쪽이 두 경우에 공통이므로 그것이 기본이고, 편의가 덧붙는 형태입니다.

7.2 언어별

언어mapset순서를 지키는가
C#Dictionary<K,int>HashSet<T>아니오 — 배열이 순서를 답니다
Java · KotlinLinkedHashMapLinkedHashSet
TypeScriptMapSet
Pythondictsetdict, set은 아니오
RubyHashSet
PHP연관 배열배열
DartMapSet — 기본값입니다
Gomap[K]Vmap[K]bool아니오 — 슬라이스가 순서를 답니다
RustHashMapHashSet아니오 — 벡터가 순서를 답니다
C++std::unordered_mapstd::unordered_set아니오 — 벡터가 순서를 답니다
SwiftDictionarySet아니오 — 배열이 순서를 답니다
Lua테이블테이블아니오 — 배열이 순서를 답니다
UnrealTMapTSet아니오 — 배열이 순서를 답니다
C없습니다 — 배열 위를 훑는 함수같습니다
html표로 냅니다표로 냅니다

순서를 지키는 컨테이너가 있으면 그것을 고릅니다. 다만 그런 컨테이너가 없는 언어가 절반이고, 그것이 다음 절입니다.

7.2.1 순서 — 이 도구가 보장하는 것과 보장하지 않는 것

적은 순서를 그대로 돌려받는 것은 쓰는 사람이 당연히 기대하는 것입니다. 그런데 절반의 언어에 순서를 지키는 해시 컨테이너가 표준으로 없습니다. 그래서 규칙을 하나로 못박습니다.

무엇보장
배열Prices.Key · Prices.Value · Tags언제나 시트에 적힌 순서입니다. 이 도구는 정렬하지 않습니다(§4)
조회를 순회한 순서보장하지 않습니다. 언어의 컨테이너가 정하고, 위 표의 마지막 칸이 언어마다 무엇인지입니다

순서가 필요하면 배열을 돕니다. 조회는 찾을 때만 씁니다. map을 순서대로 훑어야 하면 Key[j]Value[j]를 함께 도는 것이 그 방법이고, 그것은 모든 언어에서 같은 답을 냅니다. 조회를 순회하는 코드는 언어를 옮겼을 때 답이 달라집니다.

순서 있는 조회를 모든 언어에 두지 않은 이유

가능한 방법왜 안 했는가
언어의 순서 있는 컨테이너를 쓴다있는 곳에서는 이미 그렇게 합니다 — Java · Kotlin의 LinkedHashMap, Dart의 기본 Map, TypeScript · Ruby · PHP · Python(dict)이 그것입니다. 나머지 언어의 표준 라이브러리에는 그런 것이 없습니다
외부 패키지를 들인다 — Rust의 IndexMap, Swift의 OrderedDictionary생성 코드에 의존성을 더하는 것입니다. 이 도구의 산출물은 지금 표준 라이브러리 밖의 것을 하나도 요구하지 않고, 그 성질을 순회 순서와 바꾸지 않습니다
순서 있는 맵을 언어마다 직접 만든다C#·Go·Rust·C++·Swift·Lua·Unreal에 각각. 런타임이 일곱 벌 늘고, 그 일곱 벌이 인덱스 맵·참조 해석과 같은 등급의 유지 대상이 됩니다. 배열이 이미 순서를 정하는데 그 값을 위해 지불할 것이 아닙니다

대신 두 겹 표면이 이것의 답입니다. 배열은 언제나 있고 언제나 파일의 순서이므로, 순서가 필요한 코드는 어느 언어에서도 같은 것을 씁니다 — 없는 것은 「순서 있게 순회할 수 있는 조회」 하나이고, 그 하나가 없는 자리를 배열이 메웁니다.

set은 한 가지가 더 있습니다. Python의 set은 순서를 지키지 않고 dict는 지킵니다 — 같은 언어 안에서도 갈립니다. 그래서 「이 언어는 순서를 지킨다」로 외우지 말고 배열을 돈다로 외우는 것이 맞습니다.

7.3 만드는 시점

읽는 시점에 만듭니다. 지연 생성은 하지 않습니다.

근거
단순합니다배열은 어차피 만들어져 있으므로 추가 비용이 컨테이너 하나입니다
스레드 문제가 없습니다지연 생성은 첫 조회가 동시에 들어올 때를 정해야 하고, 그 규칙이 언어마다 다릅니다
되돌릴 수 있습니다계측이 비용을 드러내면 그때 지연으로 바꿉니다. 표면이 안 바뀌므로 호출하는 쪽에 영향이 없습니다

8. 익스포터

타깃setmap
json배열배열 둘{"key": [...], "value": [...]}. 오브젝트로 내지 않습니다(아래)
text번역 대상이면 원소마다 하나씩 모읍니다키는 모으지 않습니다 — 식별자는 번역하지 않습니다
binary§4§4
SQL 계열 · mongodb · redis닿지 않습니다 — 이 타깃들은 컨테이너보다 앞서 레코드 그룹 자체를 거부합니다. 컨테이너는 언제나 struct 안에 있으므로 그 거부에서 먼저 걸립니다

json의 map을 오브젝트로 내지 않는 근거 — 초판 결정의 정정

초판이 「오브젝트로 냅니다」라고 적었고 그것이 틀렸습니다. 이유가 셋입니다.

무엇내용
정수 키가 순서를 잃습니다JavaScript는 정수처럼 보이는 프로퍼티를 오름차순으로 순회합니다. {"11":120,"10":100}을 읽으면 10이 먼저 나옵니다 — 파일에 적힌 순서가 아닙니다. JSON을 되읽는 것은 TypeScript 하나인데, 그 하나에서 §7.2.1이 보장한다고 적은 배열의 순서가 무너집니다
값이 struct인 map은 오브젝트가 될 수 없습니다Value가 컬럼 여럿이므로 항목 하나에 대응하는 오브젝트가 없습니다. 오브젝트로 내면 같은 선언이 값의 종류에 따라 다른 JSON 모양을 갖게 됩니다
키 타입마다 규칙이 생깁니다bool·uuid·enum 키를 문자열로 만들고 되돌리는 규칙이 각각 필요합니다. 배열은 그런 것이 없습니다

그래서 배열 둘 그대로 냅니다. 읽기에도 나쁘지 않고 — keyvalue가 나란히 있습니다 — 무엇보다 JSON 경로와 바이너리 경로가 같은 것을 냅니다. 그 둘이 어긋나지 않는 것을 확인하는 게이트가 TypeScript 왕복 대조이고, 오브젝트로 냈다면 그 게이트가 잡았을 것을 여기서 미리 결정한 것입니다.


9. 단계와 게이트

단계무엇골든
1거절을 걷어내고 모델까지.tbs 멤버, 컬럼 확장, 셀 파싱, 중복·키 타입 검사. 생성기는 아직 이름을 대고 거절합니다무변경. 컨테이너를 쓰는 픽스처가 아직 없습니다
2언어를 하나씩. 컴파일과 되읽기 게이트를 각각. 모든 언어 끝남 — 게이트 하나가 같은 단언 한 벌로 전부를 봅니다그 언어의 골든
3text · json. 끝남json은 배열 둘 그대로이고(§8), SQL 계열은 컨테이너보다 앞서 레코드 그룹을 거부하므로 이 단계의 대상이 아닙니다무변경
4시트의 타입 칸(§2.3). 끝남map은 되고 set은 이름을 대고 거절합니다무변경

판정 기준은 .tcb 바이트 동일 짝입니다. 같은 데이터를 map<int,int>으로 적은 것과 멤버 둘짜리 레코드 배열로 적은 것이 같은 파일이어야 합니다. 합성 값 타입이 세운 방식이고, STRUCT DSL의 (나)·(다) 표기도 그것으로 확인했습니다.

set<T>T[]도 같은 짝입니다 — 중복이 없는 데이터라면 두 표기의 파일이 같아야 합니다.

워크북 3개에 파일 하나입니다. containers(선언) · containers-expanded(배열 컬럼) · containers-paired(한 셀의 쌍)이 그 셋이고, containers-refused가 거부를 봅니다.

픽스처를 더하면 xlsx-reader.tsv를 재기록합니다. 워크북마다 장·행·셀·해시가 거기 적혀 있고, 새 워크북 하나가 그 게이트를 실패하게 만듭니다.


10. 담지 않는 것

무엇
map<K,V?> — 값이 없을 수 있는 map원소 옵셔널의 비트맵이 값 컬럼에 붙으면 되지만, 참조 배열이 그 비트맵을 읽고 버리는 것이 아직 열려 있습니다. 그 답이 나온 뒤가 맞습니다
중첩 컨테이너map<int,set<int>>형식은 배열의 배열로 실을 수 있습니다. 막는 것은 셀 표기입니다 — 구분자가 세 겹이 되고, 그 자리는 sep이 푸는 문제와 같습니다
foreign참조의 키 타입이 이 패스보다 뒤에서 정해집니다. 쓸모는 분명하므로(map<foreign Item,int>) 이름을 대고 거절하고, 참조 해석 순서를 손볼 때 다시 봅니다
컨테이너의 배열 — 번호 붙은 그룹 안의 컨테이너(Bag1.Prices.Key)set<T>[]{:을} 거부하는 것과 같은 형태입니다. 컬럼 하나가 원소마다 목록을 담아야 합니다. 시트 쪽에서 닿는 같은 벽이므로 거기서도 거부합니다
정렬§4
uniqueBymap의 키 중복 검사가 그 일부를 하지만 같은 것이 아닙니다. struct 배열의 유일성은 로드맵에 있고 먼저 정할 것이 남아 있습니다