decide(tag,slot): Tag:Added/Removed vararg fix, Slot:Splice CRUD 신설
Tag:Added/:Removed가 문서상 단일 name만 받던 불일치를 vararg로 정정 (self-return 최적화는 멤버십을 매번 먼저 읽어야 해서 기각). Slot:Splice CRUD 신설 — 구간 제거+삽입을 shift/recompute 1회로 묶는 순수 최적화. Slot-in-Slot relate 범위/Animate 반환타입/Slot retract 파괴는 기존 문서와 일치 확인만. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
81ff240a59
commit
94978c78f4
4 changed files with 98 additions and 10 deletions
|
|
@ -10,6 +10,9 @@ quad의 진짜 개선점.** **[2026-08-09 세 번째 세션]** CRUD 의미론
|
|||
(`pre-implementation-audit.md` 1-7/1-8) 완전 확정, `research/
|
||||
additional-primitives-plan.md`가 다루던 키 기반 동적 컬렉션 재조정도
|
||||
`Slot:List(...)` 메소드로 이 문서에 승격·통합 완료 — 아래 참고.
|
||||
**[2026-08-12 열다섯 번째 세션]** `Splice(index, removeCount,
|
||||
...newElements)` CRUD 신설 — 구간 제거+삽입을 shift/recompute 1회로
|
||||
묶는 순수 최적화, 아래 "CRUD API 확정" 절 참고.
|
||||
|
||||
## base/roblox 패키지 경계 (2026-08-04, 5차 라운드 확정)
|
||||
|
||||
|
|
@ -324,6 +327,7 @@ Remove/Extract/Move하려 해도 참조를 안 들고 있는 경우가 잦음. `
|
|||
| `Remove` | `Slot:Remove(index)` | O(n) | 제거 **+ 파괴**(retract/Destroy) — `Extract(index):Destroy()`와 동치, 흔한 경로라 별도 이름으로 유지 |
|
||||
| `Extract` | `Slot:Extract(index, newElement?)` | O(n) 또는 O(1) | `newElement` 생략 — 제거만(파괴 안 함), 뒤 요소가 당겨져 빈 자리를 메움(O(n)). `newElement` 지정 — 그 자리를 즉시 교체(뒤 요소 안 건드림, O(1)), 이전 element를 반환 |
|
||||
| `ExtractAll` | `Slot:ExtractAll(): {T}` | O(n) | 전체 추출(파괴 안 함) — `Clear`의 비파괴 버전, 추출된 element 배열(순서 보존)을 반환 |
|
||||
| `Splice` | `Slot:Splice(index, removeCount, ...newElements): {T}` | O(n) | 한 위치에서 `removeCount`개를 비파괴 추출(반환)하고 그 자리에 `newElements`를 삽입 — shift+recompute 1회로 통합 |
|
||||
| `Clear` | `Slot:Clear()` | O(n) | 전체 `Remove`(전부 파괴) — 빈 Slot에 호출해도 no-op |
|
||||
| `Move` | `Slot:Move(oldIndex, newIndex)` | **O(n)** | 제자리 재배치 — 옛/새 위치 사이 요소들이 밀림/당겨짐(배열 splice와 동일 의미), **Parent 안 건드림** |
|
||||
| `Swap` | `Slot:Swap(indexA, indexB)` | **O(1)** | 두 인덱스의 요소를 맞교환, 나머지 안 건드림, **Parent 안 건드림** |
|
||||
|
|
@ -349,6 +353,20 @@ Remove/Extract/Move하려 해도 참조를 안 들고 있는 경우가 잦음. `
|
|||
정확히 같아서(교체도 "그 자리 걸 빼내고 새 걸 넣는" 것의 원자적 버전일
|
||||
뿐). `newElement`에도 `Add`와 같은 검증(이미 마운트/타입 제약)이
|
||||
똑같이 적용됨.
|
||||
- **`Splice` 신설(2026-08-12 열다섯 번째 세션)** — 한 구간을 제거하고
|
||||
동시에 새 요소들을 그 자리에 넣는 흔한 배치 갱신(`:List` 없이 수동
|
||||
CRUD로 큰 구간을 통째로 교체하는 경우)을 `Extract`/`Add`를 요소 수만큼
|
||||
반복 호출하면, 그때마다 개별 `raw*` 호출이 각자 shift+`recompute`를
|
||||
돌려 O(n) 비용이 반복 횟수만큼 곱으로 커짐 — `Splice`는 이걸 시프트
|
||||
1회 + `recompute` 1회로 묶는 순수 최적화(새 능력 추가 아님, `Extract`
|
||||
반복+`Add` 반복으로도 결과는 항상 재현 가능). **비파괴**(`Extract`처럼
|
||||
제거분을 파괴하지 않고 반환) — 실제 물리적 detach/reattach(Roblox
|
||||
`Parent` 조작, `AncestryChanged` 발화 등)를 언제 어떻게 할지는 base가
|
||||
정하지 않고 quad-roblox 등 백엔드 Handler 엔드포인트가 처리(기존
|
||||
"base는 추상 재조정 로직, backend는 실제 트리 조작"이라는 패키지
|
||||
경계 원칙 그대로 재적용, 새 분리 아님). 제거된 구간이 뒤 요소를 당기고
|
||||
삽입된 구간이 다시 밀어내는 게 순수하게 겹치면 상쇄되는 부분이 있어
|
||||
`Extract 반복 + Add 반복`보다 실제 이동 계산량도 더 적음.
|
||||
- **`Get`/`IndexOf` 신설, 원래 "YAGNI"로 뺐던 것을 재추가.** 처음엔
|
||||
"`:List`가 자기 key→element 맵을 따로 들고 있어 Slot 내부 상태 조회가
|
||||
불필요"하다고 판단해 드롭했으나, 위 인덱스 기준 전환과 맞물려 다시
|
||||
|
|
@ -375,16 +393,22 @@ Remove/Extract/Move하려 해도 참조를 안 들고 있는 경우가 잦음. `
|
|||
에러.
|
||||
- `Extract(index, newElement)`: `newElement`도 `Add`와 동일한 검증
|
||||
(이미 마운트/타입 제약) 적용.
|
||||
- `Splice(index, removeCount, ...newElements)`: `index`는 `Add`와 같은
|
||||
범위(1..현재 개수+1), `removeCount`는 `index`부터 실제 남은 개수를
|
||||
못 넘으면 에러(음수도 에러) — clamp 안 함, 나머지 CRUD와 같은
|
||||
fail-fast 톤. `newElements` 각각에 `Add`와 동일한 검증(이미 마운트/
|
||||
타입 제약) 적용, 검증은 실제 mutate 전에 전부 먼저 통과해야 함
|
||||
(일부만 적용된 채 중간에 에러나는 반쪽 상태 방지).
|
||||
- `Swap`: `indexA`/`indexB` 중 하나라도 범위 밖이면 에러 — 단
|
||||
`Swap(i, i)`(같은 인덱스)는 위치가 안 바뀌므로 에러 없이 no-op.
|
||||
- **`Move`/`Swap`은 반환값 없음(void)** — 내부 재배치만 수행, 멤버십
|
||||
weak-set을 안 건드림(요소가 Slot을 떠난 적이 없으므로) — 그래서 `Add`/
|
||||
`Remove`/`Extract`보다 저렴함.
|
||||
- **공개 CRUD 중 실제로 mutate하는 것(`Add`/`Remove`/`Extract`/
|
||||
`ExtractAll`/`Clear`/`Move`/`Swap`)은 "가드 확인 + `raw*` 위임"의 얇은
|
||||
wrapper** — `self._listed`(`:List`가 설치돼 있으면 수동 CRUD 금지)만
|
||||
확인하고 실제 로직은 `rawAdd`/`rawRemove`/`rawExtract`/`rawClear`/
|
||||
`rawMove`/`rawSwap`에 있음 — 이 `raw*` 함수들이 `:List`의 reconcile이
|
||||
`ExtractAll`/`Splice`/`Clear`/`Move`/`Swap`)은 "가드 확인 + `raw*` 위임"의
|
||||
얇은 wrapper** — `self._listed`(`:List`가 설치돼 있으면 수동 CRUD 금지)만
|
||||
확인하고 실제 로직은 `rawAdd`/`rawRemove`/`rawExtract`/`rawSplice`/
|
||||
`rawClear`/`rawMove`/`rawSwap`에 있음 — 이 `raw*` 함수들이 `:List`의 reconcile이
|
||||
가드 없이 직접 호출하는 바로 그 함수(아래 "`Slot:List`" 절의 "구현"
|
||||
참고). 공개 메소드에 로직이 따로 있는 게 아니라 전부 이 한 세트를
|
||||
공유. **`Get`/`IndexOf`는 순수 읽기라 이 가드 대상 아님** — `:List`가
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@
|
|||
모델은 `archive/tag-hash-key-model-reversed.md`에 원문·역전 이유 보존).
|
||||
2026-08-12 열한 번째 세션에 `TagHandler`의 `process`/`retract` 메커니즘을
|
||||
참조 카운트 기반으로 전면 정정(옛 버전은 `archive/
|
||||
retract-always-fires-reversed.md`). 새 결정만 반영, 열린 질문 없음.
|
||||
retract-always-fires-reversed.md`). 2026-08-12 열다섯 번째 세션에
|
||||
`Added`/`Removed`를 단일 이름에서 vararg로 정정(아래 값 모양 절). 새
|
||||
결정만 반영, 열린 질문 없음.
|
||||
|
||||
## 왜 재설계됐나
|
||||
|
||||
|
|
@ -18,8 +20,8 @@ retract-always-fires-reversed.md`). 새 결정만 반영, 열린 질문 없음.
|
|||
|
||||
```
|
||||
Tag(name1, name2, ...) -- 생성자, 가변인자. Tag() 빈 값도 유효
|
||||
tag:Added(name): Tag -- clone 후 이름 추가, 원본 안 건드림
|
||||
tag:Removed(name): Tag -- clone 후 이름 제거
|
||||
tag:Added(name, ...): Tag -- clone 후 이름(들) 추가, 원본 안 건드림
|
||||
tag:Removed(name, ...): Tag -- clone 후 이름(들) 제거
|
||||
tag:Contains(name): boolean -- 멤버십 확인
|
||||
tag:Apply(factory): U -- factory(self) 체이닝 설탕(Modifier와 동일 패턴)
|
||||
Tag.Merged(tag1, tag2, ...): Tag -- 여러 Tag의 합집합(무손실). Modifier의
|
||||
|
|
@ -31,9 +33,21 @@ Tag.Merged(tag1, tag2, ...): Tag -- 여러 Tag의 합집합(무손실). Modifie
|
|||
|
||||
`Added`/`Removed`가 `-ed` 어미인 이유는 **`Add`/`Remove`로 쓰면 뮤테이션
|
||||
API처럼 보이기 때문** — 실제로는 항상 `table.clone` 후 반환(Modifier
|
||||
3번 절과 동일한 immutable 확정 이유: 형제 서브트리 오염 방지). `Tag(a,b)`
|
||||
자체가 `Tag():Added(a):Added(b)`의 sugar라고 생각하면 됨 — 별도 런타임
|
||||
경로 아님.
|
||||
3번 절과 동일한 immutable 확정 이유: 형제 서브트리 오염 방지).
|
||||
|
||||
**[정정, 2026-08-12 열다섯 번째 세션] `Added`/`Removed`도 vararg —
|
||||
`Tag(a,b)`는 `Tag():Added(a,b)` 한 번의 clone sugar, `Tag():Added(a):Added(b)`
|
||||
처럼 반복 clone하는 게 아님.** 처음엔 단일 `name`만 받고 생성자의
|
||||
vararg는 반복 `Added` 호출의 sugar로 서술했으나, 이러면 이름 여러 개를
|
||||
한 번에 걸 때(`Tag(a,b,c)`도 결국 clone 1회면 충분한데) 이름 개수만큼
|
||||
`table.clone`+해싱이 반복되는 손해가 남 — 태그가 이미 걸려있는 경우 self를
|
||||
그냥 리턴하는 최적화도 검토했으나(2026-08-12 열다섯 번째 세션) 그러려면
|
||||
매번 먼저 멤버십을 읽어야 해서 오히려 전반적으로 더 비쌈(해싱이 한 번
|
||||
더 나고 Set에서도 한 번 더 남) — **기각, `Added`는 항상 새 clone을
|
||||
반환하는 현재 동작 유지**. 대신 여러 이름을 한 번의 clone으로 처리하는
|
||||
vararg만 추가해 흔한 다중 추가 케이스의 비용을 줄임. `Tag(name1, name2,
|
||||
...)` 생성자도 이제 정확히 `Tag():Added(name1, name2, ...)`(단일 clone)와
|
||||
동치.
|
||||
|
||||
**children 배열 슬롯(array-part)에 직접 놓임** — `Frame { Tag("selected") }`.
|
||||
정적으로 여러 개 놓아도(`Frame { Tag("a"), Tag("b") }`) 각자 독립적으로
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
# 2026-08-12 열다섯 번째 세션 — Slot-in-Slot relate 범위 확인, `Tag:Added` vararg, `Slot:Splice` 신설
|
||||
|
||||
사용자가 지난 며칠간 확정한 설계를 4개 항목으로 재확인, 1개는 문서 불일치를
|
||||
발견해 수정, 2개는 새 CRUD/API를 추가.
|
||||
|
||||
## 확인만 하고 넘어간 것 (문서와 일치, 변경 없음)
|
||||
|
||||
1. **Slot-in-Slot의 `slotOwner`/`kSlotMap` relate는 최상위 마운트에만
|
||||
걸림.** `attachSlot`의 중첩 재귀(`slot-plan.md` "Slot-in-Slot 중첩" 절)는
|
||||
`Dispatch.process`를 다시 안 타고 직접 재귀 호출되므로, `SlotHandler.process`
|
||||
안에서만 세팅되는 이 relate는 중첩 자식 Slot엔 전혀 안 걸림 — 완전히
|
||||
별개 처리 확인(중첩 Slot의 중복마운트 방지는 별도의 전역 element
|
||||
weak-set이 담당, 서로 안 얽힘).
|
||||
2. **`Animate`의 실제 반환 타입은 `State<Tween<T> | T>`.** `CanAnimate`가
|
||||
거짓이면 `Tween`으로 안 감싸고 plain `v`를 그대로 반환하는 분기가 이미
|
||||
`tween-plan.md`에 있음 — 확인만.
|
||||
3. **Slot의 retract는 전부 파괴, 포탈 없음.** `Extract`/`ExtractAll`로
|
||||
미리 빼낸 것만 예외 — 이미 확정된 그대로.
|
||||
|
||||
## 수정한 것
|
||||
|
||||
4. **`Tag:Added`/`:Removed`가 문서상 단일 `name`만 받고 있었음 —
|
||||
vararg로 정정.** 원래 `Tag(a,b)`를 `Tag():Added(a):Added(b)`(clone 2회)의
|
||||
sugar로 서술했는데, 사용자가 태그 여러 개를 한 번에 걸 때 이름 개수만큼
|
||||
clone+해싱이 반복되는 손해를 지적 — self-return 최적화(이미 걸려있으면
|
||||
그냥 self 반환)도 검토했으나 매번 먼저 멤버십을 읽어야 해서 오히려
|
||||
더 비싸 기각, 대신 `Added(name, ...)`/`Removed(name, ...)`로 vararg를
|
||||
지원해 여러 이름을 단일 clone으로 처리하도록 변경(`tag-plan.md`).
|
||||
|
||||
## 추가한 것
|
||||
|
||||
5. **`Slot:Splice(index, removeCount, ...newElements): {T}` 신설.**
|
||||
`ExtractAll`은 이미 있었지만, 한 구간만 비파괴 제거+삽입하는 배치
|
||||
연산은 없어서 `Extract`/`Add`를 요소 수만큼 반복하면 그때마다 개별
|
||||
shift+`recompute`가 돌아 비용이 곱으로 커지는 문제가 있었음 — 시프트
|
||||
1회+recompute 1회로 묶는 순수 최적화로 추가(새 능력이 아니라 기존
|
||||
CRUD 반복으로도 재현 가능한 결과, 비용만 다름). 비파괴(제거분은 파괴
|
||||
안 하고 반환) — 실제 물리 detach/reattach는 기존 base/roblox 패키지
|
||||
경계 그대로 backend Handler 몫(`slot-plan.md`).
|
||||
11
CLAUDE.md
11
CLAUDE.md
|
|
@ -627,3 +627,14 @@ unbindLifetime만 있고 slot 자신의 앵커/해제가 빠져 있었음).
|
|||
규칙으로 명문화 — 앞으로 비슷한 설계에서 Slot 사례를 매번 재발굴하지
|
||||
않도록. 열한~열네 번째 세션에 걸친 "retract는 항상 불림" 정정과 그
|
||||
파생 GC 이슈 시리즈가 이걸로 마무리됨.
|
||||
|
||||
**2026-08-12 열다섯 번째 세션 — Slot-in-Slot relate 범위 확인, `Tag:Added`
|
||||
vararg, `Slot:Splice` 신설** (`session/2026-08-12-15-slot-in-slot-relate-scope-tag-splice-additions.md`)
|
||||
사용자가 최근 확정 설계 4개를 재확인 질문 — 3개(Slot-in-Slot의
|
||||
`slotOwner`/`kSlotMap` relate가 최상위 마운트에만 걸림, `Animate` 반환
|
||||
타입이 `State<Tween<T>|T>`, Slot retract는 전부 파괴·포탈 없음)는 문서와
|
||||
일치해 확인만. 1개(`Tag:Added`/`:Removed`가 문서상 단일 `name`만 받던 것)는
|
||||
불일치 발견해 vararg로 정정(self-return 최적화는 매번 멤버십을 먼저 읽어야
|
||||
해서 오히려 더 비싸 기각, `tag-plan.md`). 추가로 `Slot:Splice(index,
|
||||
removeCount, ...newElements)` CRUD 신설 — 구간 제거+삽입을 shift/recompute
|
||||
1회로 묶는 순수 최적화(`slot-plan.md`).
|
||||
|
|
|
|||
Loading…
Reference in a new issue