decide(attribute): group Attribute(...) primitive, AttributeKey rename, weak-cache identity
Adds Attribute(store1, store2, ...) as a Tag-shaped array-part value
object that projects one or more Stores' named Source fields onto
native Roblox attributes in one binding, with .Merged for combining
heterogeneous Stores. Rejected: a bare [Attribute] = Store{...} hash
slot (only one slot per instance, can't merge heterogeneous stores)
and making Attribute a Store subtype (would let Store<T>'s T be a
Store again, colliding with the existing "handler-layer values can't
live in Source" rule). Verified the value-slot aggregation in .Merged
doesn't reopen the earlier "layered Store" rejection (archive/
context-rejected.md) since it's an explicit one-time author-time
composition, not an implicit read-time parent-chain fallback.
Renamed the existing single-key constructor Attribute<<T>>(name) to
AttributeKey<<T>>(name) to remove the name collision with the new
group primitive (provisional per existing OnChange/OnChangeKey
precedent; final bikeshed still queued in question.md).
Follow-up: AttributeKey(name) now memoizes through a name-keyed weak
table, guaranteeing AttributeKey(a) == AttributeKey(a) while any
strong reference (e.g. a live Dispatch chain entry) keeps it alive.
This let the group Attribute handler drop its originally-planned
self-contained SetAttribute/subscription logic and instead recurse
into the existing single-key AttributeKey dispatch path per field,
reusing its None/retract/store-bind handling instead of duplicating
it — the group handler's own state shrinks to just a prior-name-set
diff for deciding which keys to retractUnder. The same memoization
was applied to OnChangeKey for the same reason (pure name-to-key
mapping, no other varying state), confirmed safe even once
State<function> callbacks are involved.
Reflected across base/attribute-plan.md, base/onchange-plan.md,
base/architecture.md (source tree + Brand isX list), ROADMAP.md M2/M10,
question.md, README.md, and the luau-test type-narrowing spike.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
1f56c75978
commit
6216f12f37
12 changed files with 436 additions and 69 deletions
|
|
@ -40,8 +40,8 @@
|
|||
| `effect-plan.md` | **[2026-08-07 신설, 여섯 번째 세션에 확정]** `Effect(fn, state?)` — `state` 없으면 설치 1회+leaf 사망 시 확정 정리, 있으면 내부적으로 `state:Observer(...)`를 조합해 재실행+cleanup 체이닝(React `useEffect` 동형). Observer와의 관계 해소 완료 |
|
||||
| `ui-shorthand-plan.md` | **[2026-08-07 `research/`에서 승격]** `UICorner`/`UIPadding`/`UIScale` 인라인 편의 키 — 이름(v1 `Corner`/`PaddingAll`/`Scale`에서 Modifier 필드명과 안 겹치게 `UI` 프리픽스로 확정)·메커니즘(Handler)·패키지 배치(quad-roblox 코어)·store-bind 가능성까지 전부 확정. 이미지 라운드 트릭(`RoundSize`)은 드롭 — `archive/ui-shorthand-roundsize-dropped.md` 참고. `v=nil`이면 `process` 자신이 만든 자식 제거(`retract` 아님) |
|
||||
| `tag-plan.md` | **[2026-08-08 세 번째 세션 재설계]** `Tag(...)` — array-part 값 객체, `Modifier`와 같은 immutable clone 체이닝(`:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`), `CollectionService` 글루만 quad-roblox. 이제 `retract`가 의미 있음(타입이 바뀌면 전체 삭제, 같은 Tag끼리는 `process`가 diff). 구 해시 파트 boolean 모델은 `archive/tag-hash-key-model-reversed.md` |
|
||||
| `attribute-plan.md` | **[2026-08-07 여덟 번째 세션 신설]** `[Attribute "Name"]` — `SetAttribute(name, nil)`이 네이티브 지우기라 `None` 센티널과 가장 깔끔하게 맞아떨어짐, `retract` 불필요. 타입 파라미터화 이름(`Attribute<T>` vs `BooleanAttribute`류)만 미확정 |
|
||||
| `onchange-plan.md` | **[2026-08-10 세션 신설]** `OnChange(name)` — `GetPropertyChangedSignal` 바인딩 전용 DI 키, `Attribute`와 달리 제네릭 타입 파라미터 없음(콜백 타입은 인라인 명시, 이벤트 바인딩과 같은 급 트레이드오프). 전부 quad-roblox(`Handlers/OnChange.luau`), `State<function>`은 기존 이벤트 store-bind 메커니즘 재사용 |
|
||||
| `attribute-plan.md` | **[2026-08-07 여덟 번째 세션 신설]** 단일 키 `[AttributeKey<T> "Name"]`(구 `Attribute<T>`) — `SetAttribute(name, nil)`이 네이티브 지우기라 `None` 센티널과 가장 깔끔하게 맞아떨어짐, `retract` 불필요. **[2026-08-11 아홉 번째 세션]** 여러 Store를 한 번에 attribute로 묶는 그룹 `Attribute(...)` 프리미티브 신설(`Tag`와 동형 array-part 값 객체, `Merged`로 헤테로지니어스 Store 합성), 이름 충돌 방지로 단일 키를 `AttributeKey`로 리네임(잠정). **[같은 세션 후속]** `AttributeKey(name)`이 이름별 weak 캐시로 동등성 보장하도록 확정되며, 그룹 Handler는 자기 완결형 재구현 대신 메모이즈된 키로 기존 단일 키 경로에 재귀 위임하는 걸로 개정(중복 구현 제거) |
|
||||
| `onchange-plan.md` | **[2026-08-10 세션 신설]** `OnChange(name)` — `GetPropertyChangedSignal` 바인딩 전용 DI 키, `Attribute`와 달리 제네릭 타입 파라미터 없음(콜백 타입은 인라인 명시, 이벤트 바인딩과 같은 급 트레이드오프). 전부 quad-roblox(`Handlers/OnChange.luau`), `State<function>`은 기존 이벤트 store-bind 메커니즘 재사용. **[2026-08-11 아홉 번째 세션 후속]** `AttributeKey`와 동일한 이름별 weak 캐시로 `OnChange(a) == OnChange(a)` 동등성 보장 |
|
||||
| `relate-plan.md` | **[2026-08-08 신설]** `Relate` — `inst`를 weak 키로 하는 범용 릴레이션 프리미티브(`SetWeak`/`GetWeak`/`SetStrong`/`GetStrong`, 비싱글톤 생성자). 구 `base.perInstanceState(inst)` placeholder를 대체·정식 승격, `lifecycle-pattern.md`의 `bindLifetime`/`canExecute`가 그 위에 얹힘 |
|
||||
|
||||
## `reference/` — 온디맨드 참고 자료 (2026-08-07 신설)
|
||||
|
|
|
|||
|
|
@ -26,12 +26,16 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo
|
|||
테이블을 계속 쌓는 방식, `reference/quad-v1-architecture.md` 참고)은 폐기.
|
||||
store 바인드에 대한 변경은 "전체 변경"으로 간주(UB 아님, 문서화된 의미론) —
|
||||
부분 복사/오버레이가 필요하면 팩토리 함수로 필요한 곳만 명시적으로 복사.
|
||||
4. **PA님 스타일 DI 키 계속 지원**: `[Attribute "Name"]` 같은 특수 바인드 키,
|
||||
store 컴퓨티드 바인드도 가능해야 함(`retract`, 구 cleanup,
|
||||
`base/lifecycle-pattern.md` 참고). **[정정, 2026-08-08 세 번째 세션]**
|
||||
`Tag`는 더 이상 `[Tag ""] = true` 해시 파트 DI 키가 아님 — array-part
|
||||
값 객체(`Tag(...)`)로 재설계됨, `base/tag-plan.md` 참고
|
||||
(`archive/tag-hash-key-model-reversed.md`에 구 모델 보존).
|
||||
4. **PA님 스타일 DI 키 계속 지원**: `[AttributeKey "Name"]`(구 `Attribute`,
|
||||
2026-08-11 아홉 번째 세션에 그룹 `Attribute(...)`와 이름 충돌 방지로
|
||||
리네임) 같은 특수 바인드 키, store 컴퓨티드 바인드도 가능해야 함
|
||||
(`retract`, 구 cleanup, `base/lifecycle-pattern.md` 참고). **[정정,
|
||||
2026-08-08 세 번째 세션]** `Tag`는 더 이상 `[Tag ""] = true` 해시 파트
|
||||
DI 키가 아님 — array-part 값 객체(`Tag(...)`)로 재설계됨,
|
||||
`base/tag-plan.md` 참고(`archive/tag-hash-key-model-reversed.md`에 구
|
||||
모델 보존). **[2026-08-11 아홉 번째 세션]** `Attribute(...)`도 여러
|
||||
Store를 한 번에 attribute로 묶는 array-part 값 객체로 신설(`Tag`와
|
||||
동형), `base/attribute-plan.md` 참고.
|
||||
5. **id 기반 전역 조회 폐지, Tag 시스템으로 대체.** v1의 `Store.GetObject(id)`/
|
||||
`Frame "id" {}`류는 더 이상 없음 — "id 매핑이 비현실적"이라는 게 이유.
|
||||
네임스페이싱 문제는 있지만 별도 네임스페이스 개념을 추가하면 라이브러리
|
||||
|
|
@ -138,6 +142,7 @@ quad/
|
|||
│ ├── Blocker.luau # 값 기반 emit 지연/합치기(`base/blocker-plan.md`), State/Source와 밀접 연관돼 같은 위치
|
||||
│ ├── Modifier.luau # flatten-before-dispatch, immutable 체이닝, 제네릭 `__index` 필드 setter 합성 + `:Apply`/`:Peek`/`Overridden`(`base/modifier-plan.md`)
|
||||
│ ├── Tag.luau # 값 타입+immutable clone 체이닝(`Tag(...)`/`:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`), CollectionService 글루는 quad-roblox Handlers/Tag.luau(`base/tag-plan.md`, 2026-08-08 세 번째 세션)
|
||||
│ ├── Attribute.luau # 그룹 값 타입+API(`Attribute(store1, store2, ...)`/`Merged`, `Tag`와 동형) — `SetAttribute` 글루는 quad-roblox Handlers/Attribute.luau(`base/attribute-plan.md`, 2026-08-11 아홉 번째 세션). 단일 키(`AttributeKey<<T>>`)는 값 타입 레이어 없이 quad-roblox 단독 소속(아래)
|
||||
│ ├── Tween.luau # 값 타입만(`Tween(opts)` 팩토리, `isTween`/`TweenTag`) — 엔진 무관, 독립 Dispatch 핸들러 아님. 실제 애니메이션 처리는 quad-roblox Handlers/Property.luau 내부 분기(`research/tween-plan.md`, 2026-08-10 세션 재설계)
|
||||
│ ├── Effect.luau # `Effect(fn, state?)` — state 없으면 설치1회+leaf사망시 정리, 있으면 State.Observer를 조합해 재실행(`base/effect-plan.md`)
|
||||
│ ├── Dispatch/
|
||||
|
|
@ -159,8 +164,9 @@ quad/
|
|||
├── Handlers/
|
||||
│ ├── Property.luau # 일반 프로퍼티 세팅 + `isTween(realv)` 분기(3-상태 릴레이션 슬롯 `RobloxTween|true|nil`, hasBeenSet 억제, override 정책) — 구 `Handlers/Tween.luau`(높은 우선순위 store-bind 핸들러)는 폐기(`archive/tween-special-bind-key-reversed.md`)
|
||||
│ ├── Event.luau # ReflectionService 기반 자동 판별
|
||||
│ ├── OnChange.luau # `OnChange(name)` DI 키 팩토리+Handler, `GetPropertyChangedSignal` 바인딩(`base/onchange-plan.md`, 2026-08-10 세션)
|
||||
│ ├── Attribute.luau
|
||||
│ ├── OnChange.luau # `OnChange(name)` DI 키 팩토리+Handler, `GetPropertyChangedSignal` 바인딩 + 이름별 weak 캐시(`AttributeKey`와 동일 기법, `base/onchange-plan.md`, 2026-08-10 세션)
|
||||
│ ├── AttributeKey.luau # 단일 키(`AttributeKey<<T>>(name)`/`BooleanAttribute`류) DI 키 팩토리+Handler, `SetAttribute`/`None` 지우기 + 이름별 weak 캐시(동등성 보장, `base/attribute-plan.md` "동등성" 절)
|
||||
│ ├── Attribute.luau # 그룹(`Attribute(store1, store2, ...)`) process/retract — 이름 집합 diff만 자체 로직, 실제 `SetAttribute`/구독은 메모이즈된 `AttributeKey(name)`로 `Dispatch.process`/`retractUnder`에 재귀 위임(단일 키 경로 재사용, 중복 구현 없음) — 값 타입/API는 quad-base Attribute.luau(`base/attribute-plan.md`)
|
||||
│ ├── Tag.luau # CollectionService 글루만(process/retract) — 값 타입/API는 quad-base Tag.luau(`base/tag-plan.md`)
|
||||
│ ├── Slot.luau # base Slot 재조정 로직의 실제 적용/해제(Instance Parent 조작)
|
||||
│ └── InstanceChild.luau # k:number, v:Instance — 중첩 인스턴스 자식(예: Frame { Frame {} })
|
||||
|
|
|
|||
|
|
@ -1,20 +1,26 @@
|
|||
# Attribute 특수 키 — 타입 파라미터화, `SetAttribute(name, nil)` 네이티브 지우기
|
||||
# Attribute — 단일 키(`AttributeKey`)와 그룹(`Attribute(...)`) 두 프리미티브
|
||||
|
||||
**상태**: base — 메커니즘/`None`/`retract` 동작뿐 아니라 타입 파라미터화도
|
||||
**둘 다 채택으로 확정**(2026-08-09 열한 번째 세션, 아래 참고). `[Attribute
|
||||
"Name"]` DI 키의 존재 자체는 `architecture.md`
|
||||
4번 항목에서 이미 확정. UICorner 숏핸드/Tween처럼 별도 전용 문서가 없던 걸
|
||||
2026-08-07 여덟 번째 세션에 메꿈("1 프리미티브 1 파일" 관례를 Tag/Attribute
|
||||
에도 적용해야 한다는 사용자 지적) — `bind-system-plan.md`의 "Attribute
|
||||
특수 키 — 타입 파라미터화" 절(2026-08-06 신설) 내용을 그대로 옮기고, 오늘
|
||||
논의한 `None`/`process`/`retract` 동작을 추가.
|
||||
**상태**: base — 단일 키 메커니즘/`None`/`retract` 동작과 타입 파라미터화는
|
||||
전부 확정(2026-08-09 열한 번째 세션). **[2026-08-11 아홉 번째 세션 추가]**
|
||||
Store 여러 개를 한 번에 attribute로 묶어 바인드하는 그룹 `Attribute(...)`
|
||||
프리미티브 신설, 이름 충돌 방지를 위해 기존 단일 키 생성자를
|
||||
`Attribute<<T>>` → `AttributeKey<<T>>`로 리네임(잠정 확정 — 최종 이름은
|
||||
여전히 `.claude/question.md` 용어정리 대기열). `[AttributeKey "Name"]`(구
|
||||
`[Attribute "Name"]`) DI 키의 존재 자체는 `architecture.md` 4번 항목에서
|
||||
이미 확정. UICorner 숏핸드/Tween처럼
|
||||
별도 전용 문서가 없던 걸 2026-08-07 여덟 번째 세션에 메꿈("1 프리미티브 1
|
||||
파일" 관례를 Tag/Attribute에도 적용해야 한다는 사용자 지적) —
|
||||
`bind-system-plan.md`의 "Attribute 특수 키 — 타입 파라미터화" 절(2026-08-06
|
||||
신설) 내용을 그대로 옮기고, 논의한 `None`/`process`/`retract` 동작을 추가.
|
||||
|
||||
## 문제 — 타입 있는 값이라 Luau가 좁혀줄 방법이 필요
|
||||
## 단일 키 — `AttributeKey<<T>>` (구 `Attribute<<T>>`)
|
||||
|
||||
### 문제 — 타입 있는 값이라 Luau가 좁혀줄 방법이 필요
|
||||
|
||||
Roblox Attribute는 Instance/Tag와 달리 실제로 **타입이 있는 값**
|
||||
(string/boolean/number/Color3/UDim/UDim2/Vector2/Vector3/CFrame/Instance
|
||||
참조 등 제한된 프리미티브 집합, 테이블 등 복합 타입은 지원 안 함)이라, 그냥
|
||||
`[Attribute "name"] = value`로 두면 `value`의 타입을 Luau가 좁혀줄 방법이
|
||||
`[AttributeKey "name"] = value`로 두면 `value`의 타입을 Luau가 좁혀줄 방법이
|
||||
없음. 커스텀/복합 데이터(테이블 등)는 애초에 Attribute가 지원을 안 하므로
|
||||
Ref(직접 참조 획득) 쪽으로 빠지는 게 맞고, Attribute는 프리미티브 전용으로
|
||||
남기면 된다는 게 사용자 판단 — Value 오브젝트가 역사적으로 Attribute의
|
||||
|
|
@ -25,11 +31,13 @@ Instance 참조 타입도 지원해서 `ObjectValue` 없이도 Ref 용도로 Att
|
|||
지원까지 감안하면 그 결정의 근거가 한층 더 탄탄해짐).
|
||||
|
||||
**확정(2026-08-09 열한 번째 세션) — 둘 다 채택**:
|
||||
- `[Attribute<<boolean>> "name"] = true` (리터럴 또는 store-bind 값) —
|
||||
- `[AttributeKey<<boolean>> "name"] = true` (리터럴 또는 store-bind 값) —
|
||||
제네릭 파라미터로 타입을 명시하는 제네릭 생성자 스타일. 기본/범용 경로.
|
||||
- `[BooleanAttribute "name"] = true` — 타입별로 이름이 다른 정적 생성자
|
||||
패밀리(`StringAttribute`/`NumberAttribute`/`Color3Attribute`/
|
||||
`InstanceAttribute` 등). 실사용 빈도가 높은 몇 개만 지름길로.
|
||||
`InstanceAttribute` 등). 실사용 빈도가 높은 몇 개만 지름길로. **이름은
|
||||
`Attribute`가 아니라 이미 타입별로 갈라져 있어 아래 그룹 `Attribute(...)`와
|
||||
겹치지 않음 — 리네임 대상 아님.**
|
||||
|
||||
**근거**: 이미 확정된 DI 인스턴스 생성 패턴(`bind-system-plan.md` "인스턴스
|
||||
생성 / 이벤트 네이밍 인체공학" 절)과 구조적으로 똑같은 문제라 같은 결론
|
||||
|
|
@ -39,7 +47,7 @@ Instance 참조 타입도 지원해서 `ObjectValue` 없이도 Ref 용도로 Att
|
|||
호출부가 타입을 어떻게 명시하느냐(제네릭 파라미터 vs 이름)뿐이라 어느
|
||||
쪽을 쓰든 런타임 동작에 차이 없음.
|
||||
|
||||
**[실측 필요, M0/M10]** `[Attribute<<boolean>> "name"] = value`처럼 DI
|
||||
**[실측 필요, M0/M10]** `[AttributeKey<<boolean>> "name"] = value`처럼 DI
|
||||
키 제네릭 파라미터로 `=` 뒤 `value`의 타입까지 실제로 좁혀지는지는
|
||||
미검증 — Luau 솔버가 이 조합을 못 풀면 `value`가 `any`로 남을 수 있음.
|
||||
단, **타입 추론이 안 되더라도 런타임 동작에는 영향 없음**(순수 정적
|
||||
|
|
@ -47,7 +55,51 @@ Instance 참조 타입도 지원해서 `ObjectValue` 없이도 Ref 용도로 Att
|
|||
되면 `BooleanAttribute` 같은 정적 타입 패밀리 쪽이 사실상 유일하게
|
||||
믿을 수 있는 정적 체크 경로가 됨.
|
||||
|
||||
## 메커니즘, `None`, `retract` — 전부 확정 (2026-08-07 여덟 번째 세션)
|
||||
### 동등성 — 이름별 weak 캐시로 `AttributeKey(name) == AttributeKey(name)` 보장 (2026-08-11 아홉 번째 세션 후속)
|
||||
|
||||
**확정**: `AttributeKey<<T>>(name)`(및 `BooleanAttribute(name)` 등 정적
|
||||
패밀리 전부 — 아래 참고)는 내부적으로 이름별 weak 캐시를 거침:
|
||||
|
||||
```lua
|
||||
local cache = setmetatable({}, { __mode = "v" }) -- 값만 weak
|
||||
local function AttributeKey(name)
|
||||
local cached = cache[name]
|
||||
if cached then return cached end
|
||||
local real = -- 진짜 생성(Brand 부여 등)
|
||||
cache[name] = real
|
||||
return real
|
||||
end
|
||||
```
|
||||
|
||||
- **캐시 키는 순수 문자열 `name`뿐, 제네릭 파라미터 `T`는 안 씀** —
|
||||
`T`는 런타임에 아무 영향 없는 순수 정적 타입 트릭(위 "근거" 절의
|
||||
"내부 구현은 완전히 동일" 그대로)이라, `AttributeKey<<boolean>>("Enabled")`와
|
||||
`AttributeKey<<number>>("Enabled")`는 실제로 **완전히 같은 런타임
|
||||
객체**를 돌려받음(호출부에서 다른 정적 타입으로 캐스팅될 뿐). 같은
|
||||
이유로 `BooleanAttribute("Enabled")`도 같은 캐시를 공유해 동일 객체를
|
||||
반환해야 함 — "내부 구현이 완전히 동일하다"는 기존 확정이 객체
|
||||
identity 수준까지 이제 실제로 보장됨.
|
||||
- **값만 weak라서 "쓰는 도중엔 항상 같은 게 리턴, 다 쓰고 나면 자연히
|
||||
풀림"**: 어딘가(Dispatch의 `(inst,k)`별 핸들러 체인 등)가 이 키
|
||||
객체를 강한 참조로 붙들고 있는 동안은 캐시 엔트리도 계속 살아있어
|
||||
같은 이름으로 다시 호출해도 항상 같은 객체가 나옴. 아무도 안 붙들게
|
||||
되면(그 이름의 attribute 바인딩이 완전히 retract됨) GC가 캐시 엔트리를
|
||||
걷어가고, 그 다음에 같은 이름을 다시 부르면 새 객체가 생기는데 —
|
||||
이 시점엔 이전 객체를 참조하는 곳이 아무도 없었으므로 identity가
|
||||
달라져도 문제 될 게 없음.
|
||||
- **`Tag`는 이 기법이 안 맞음**(비교 확인) — `AttributeKey(name)`은
|
||||
"이름 → 키" 외에 다른 가변 정보가 없는 순수 매핑이라 이름만으로
|
||||
캐시가 성립하지만, `Tag(...)`의 값은 내부 이름 목록 자체가 매번
|
||||
달라지는 게 핵심(`:Added`/`:Removed`로 계속 다른 집합을 표현)이라
|
||||
"캐시할 안정적인 키"가 애초에 없음 — 동등성 비교/캐싱이 의미가 없는
|
||||
이유가 이거.
|
||||
- **[반영 완료] `OnChange(name)`도 같은 모양**(이름 → 키, 다른 가변
|
||||
정보 없음)이라 같은 기법 그대로 적용 — `State<function>`이 되더라도
|
||||
캐시는 키 객체 identity만 다루므로 문제 없고, `OnChange "a" == OnChange
|
||||
"a"`가 외부에 관찰되는 것도 의도적으로 허용 가능한 동작(사용자 확인).
|
||||
`base/onchange-plan.md` "확정" 절 참고.
|
||||
|
||||
### 메커니즘, `None`, `retract` — 전부 확정 (2026-08-07 여덟 번째 세션)
|
||||
|
||||
타입 파라미터화 이름과 무관하게 런타임 동작은 확정:
|
||||
|
||||
|
|
@ -59,7 +111,7 @@ Instance 참조 타입도 지원해서 `ObjectValue` 없이도 Ref 용도로 Att
|
|||
`inst:SetAttribute(name, nil)`을 그대로 호출하면 끝 — UICorner 숏핸드처럼
|
||||
"만들어둔 자식을 수동으로 찾아 지우는" 로직조차 필요 없음.
|
||||
- **`retract` 불필요** — Tag와 같은 이유: 값이 뭐든(실제 값/`nil`) 항상
|
||||
같은 `AttributeHandler`가 이 키를 계속 담당(핸들러 *타입*이 안 바뀜).
|
||||
같은 `AttributeKeyHandler`가 이 키를 계속 담당(핸들러 *타입*이 안 바뀜).
|
||||
`retract`가 의미 있는 유일한 패턴("매치되는 핸들러 타입 자체가 바뀜",
|
||||
`Tag(...)`↔`nil`이 실사례 — 2026-08-10 세션부터 Tween은 더 이상 이
|
||||
패턴의 예시가 아님, `research/tween-plan.md`)에 해당 안 함 —
|
||||
|
|
@ -68,12 +120,122 @@ Instance 참조 타입도 지원해서 `ObjectValue` 없이도 Ref 용도로 Att
|
|||
- store-bind 가능(일반 프로퍼티와 동일하게 취급, `Store<T>`/`State<T>`
|
||||
값도 받음).
|
||||
|
||||
## 패키지 배치
|
||||
## 그룹 `Attribute(...)` — 여러 Store를 한 번에 attribute로 (2026-08-11 아홉 번째 세션 신설)
|
||||
|
||||
### 동기
|
||||
|
||||
Store 필드 여러 개를 각각 `[AttributeKey<<T>> "name"] = store.name`으로
|
||||
나열하는 건, 이미 이름 붙은 typed Source 모음(Store)이 있는 상황에서
|
||||
번거로움 — Store의 타입 체크/reactive 인프라를 attribute에도 그대로
|
||||
재활용하고 싶다는 요구에서 출발.
|
||||
|
||||
### 검토했다 기각한 대안
|
||||
|
||||
- **`[Attribute] = Store {...}` (해시파트 단일 슬롯)**: 인스턴스당 이
|
||||
키 슬롯이 하나뿐이라, 헤테로지니어스한 Store 여러 개(예: 스타일
|
||||
Store + 상태 Store)를 한 인스턴스에 동시에 반영할 방법이 구조적으로
|
||||
없음 — 기각.
|
||||
- **`Attribute`를 Store의 서브타입/확장으로**: Attribute가 Store를
|
||||
상속(IS-A)하면 `Store<T>`의 `T`가 다시 Attribute(=Store)일 수 있게
|
||||
되어, 이미 확정된 제약("핸들러 계층 값은 Source에 못 들어감" —
|
||||
`store-semantics.md`의 `Store<T>`의 `T`는 Modifier 불가 규칙과 같은
|
||||
이유)과 부딪히는 "Store 안에 Store"를 실제로 만들어냄. Attribute는
|
||||
Store를 **참조(HAS-A)**만 해야지 **상속(IS-A)**하면 안 됨 — 기각.
|
||||
|
||||
### 채택안 — `Tag`와 동형인 array-part 값 객체
|
||||
|
||||
```
|
||||
Attribute(store1, store2, ..., {plain = "table도 됨"}) -- 생성자, 여러 개 받음
|
||||
Attribute.Merged(a, b, ...): Attribute -- Tag.Merged와 동일 이유(헤테로지니어스 합성)
|
||||
Frame { Attribute(styleStore), Attribute(stateStore) } -- 여러 개 나란히 둬도 각자 자기 키만 반영(Tag와 동일)
|
||||
```
|
||||
|
||||
`Attribute.Merged`가 내부적으로 하는 일은 각 Store에서 이름 붙은 `Source`
|
||||
슬롯을 그대로 가져와 자기 자신의 key→Source 맵에 넣는 것 — 아래 "레이어드
|
||||
Store 기각과 안 부딪히나" 참고.
|
||||
|
||||
### 메커니즘 — 메모이즈된 키로 기존 단일 키 경로에 재귀 위임
|
||||
|
||||
**[2026-08-11 아홉 번째 세션 후속, 개정]** 최초안은 "자기 완결형 Handler,
|
||||
Dispatch 재진입 없이 직접 `SetAttribute`+수동 per-field StoreBind 구독"
|
||||
이었으나, 위 "동등성" 절의 이름별 weak 캐시가 확정되며 그 회피 이유
|
||||
자체가 없어짐 — `AttributeKey(name)`을 몇 번을 다시 불러도 항상 같은
|
||||
객체가 나오므로, Dispatch의 `(inst, k)`별 핸들러 체인 추적이 깨질
|
||||
위험이 없음. 그래서 그룹 Handler는 **자기만의 SetAttribute/구독 로직을
|
||||
새로 만들지 않고, 각 필드를 기존 단일 키 `AttributeKey` 경로에 그대로
|
||||
재귀 위임** — `None`/`retract`/store-bind 전부 이미 확정된 단일 키
|
||||
메커니즘을 100% 재사용, 중복 구현 없음:
|
||||
|
||||
- **그룹 값이 (재)할당될 때**(마운트 시점, 또는 `:Compute`가 그룹 값
|
||||
자체를 교체하는 드문 경우 — 흔한 필드 하나 변경과는 다른 경로, 아래
|
||||
참고): `(inst, index)`(array-part 위치, `Tag`의 `relate:GetStrong(inst,k)`와
|
||||
동일 키잉 — `k`는 배열 인덱스)로 찾은 릴레이션에 저장된 **"이전에 쓴
|
||||
attribute 이름 문자열 집합"**과 새 값의 키 집합을 diff:
|
||||
- 사라진 이름만 `Dispatch.retractUnder(inst, AttributeKey(name))` —
|
||||
메모이즈된 키라 그 이름이 살아있는 동안 만들어졌던 원래 체인과
|
||||
정확히 같은 슬롯을 가리켜 정리됨.
|
||||
- 남아있거나 새로 들어온 이름은 **값 비교 없이 전부**
|
||||
`Dispatch.process(inst, AttributeKey(name), source)`로 넘김 — 작성자가
|
||||
직접 `[AttributeKey<<T>> name] = source`를 쓴 것과 완전히 같은 경로를
|
||||
타므로, `source`가 `State`/`Source`면 `Dispatch/StoreBind`가 알아서
|
||||
언랩+구독까지 다 해줌(그룹 Handler가 따로 구독 관리 안 함).
|
||||
- **값 비교(`:Get()`으로 old/new 비교)는 하지 않음** — State 계약("값은
|
||||
항상 선언된 Compute 재실행 결과, 캐시 비교 금지", `store-semantics.md`
|
||||
"하드 경계" 절)과 어긋나고, 릴레이션 키를 문자열 이름 집합(존재 여부)만
|
||||
들고 있으면 충분해서 굳이 값까지 비교할 이유가 없음.
|
||||
- **필드 하나만 바뀌는 흔한 경우**(`storeA.foo:Set(v)`, 그룹 자체는
|
||||
안 바뀜)는 위 그룹 재처리를 아예 거치지 않음 — 마운트 시 이미 걸린
|
||||
단일 키 `AttributeKeyHandler`의 store-bind 구독이 바로
|
||||
`SetAttribute("foo", v)`를 호출(그룹 재진입 없이 그 경로 스스로).
|
||||
**`AttributeChanged`/`GetAttributeChangedSignal` 남발 걱정 없음** —
|
||||
키 집합이 안 바뀌는 한 diff 로직 자체가 안 돎.
|
||||
- **`retract`**: 자기가 쓴 키 전부 `Dispatch.retractUnder(inst,
|
||||
AttributeKey(name))`로 정리 — 단일 키의 `SetAttribute(name, nil)`이
|
||||
그대로 실행되므로 결과적으로 `Tag`와 동일하게 확실히 청소됨. Roblox는
|
||||
Instance 풀링/재사용이 흔해서, 이전 컴포넌트가 남긴 attribute가
|
||||
재사용된 Instance에 방치되면 selector/스타일시트 로직에 실제 버그가
|
||||
됨. ("소진돼도 부작용 없으니 방치해도 된다"는 초안은 기각 — Tag의
|
||||
확실한 청소 원칙과 통일.)
|
||||
|
||||
**그룹 Handler에 남는 자기 로직은 사실상 "이름 집합 diff"뿐** — 실제
|
||||
`SetAttribute` 호출/`None` 처리/store-bind 구독은 전부 기존 단일 키
|
||||
경로가 그대로 담당. `TagHandler`가 `CollectionService` 호출을 직접 하는
|
||||
것과 달리, 여기서는 그 실행 자체를 위임한다는 점이 다름(Attribute만
|
||||
"이미 완성된 재사용 가능한 단일 키 경로"가 있어서 가능한 차이 — Tag는
|
||||
애초에 이름 하나짜리 단일 키 대응물이 없음).
|
||||
|
||||
### `Attribute.Merged`가 레이어드 Store 기각과 안 부딪히나 — 점검, 안 부딪힘
|
||||
|
||||
`archive/context-rejected.md`가 기각한 "레이어드 Store"는 **읽는 시점에
|
||||
부모 체인을 암묵적으로 거슬러 올라가는 자동 폴백**(`__index` 체인, "이
|
||||
값이 왜 이거지"를 추적하려면 부모 체인을 다 훑어야 하는 디버깅 불투명성)이
|
||||
문제였음. `Attribute.Merged`는:
|
||||
|
||||
- **작성 시점에 명시적으로 한 번** 여러 Store의 `Source` 참조를 평탄한
|
||||
자기 맵으로 모으는 것 — 런타임 암묵 폴백 체인이 없음(읽을 때 부모를
|
||||
거슬러 올라가지 않음, 그냥 자기 맵을 봄).
|
||||
- 범용 값 컨테이너의 레이어링이 아니라, `Modifier`의 `Overridden`("이미
|
||||
계산된 걸 합침")과 같은 **목적성 있는 값 객체의 조립** — dispatch에
|
||||
참여하기 위해 만들어진 전용 값 객체지, "아무 값이나 담는 컨테이너"가
|
||||
레이어링되는 게 아님.
|
||||
|
||||
기각 사유(범용 컨테이너의 암묵적 런타임 폴백)가 이 케이스엔 적용 안 됨 —
|
||||
새 primitive 추가에 문제없음.
|
||||
|
||||
### 패키지 배치 — Tag와 동일 원칙
|
||||
|
||||
값 타입+API(`Attribute(...)`/`Merged`)는 quad-base(엔진 무관, 순수 데이터+
|
||||
연산). `SetAttribute` 실제 호출 글루만 quad-roblox.
|
||||
|
||||
## 패키지 배치 (단일 키 `AttributeKey`)
|
||||
|
||||
UICorner 숏핸드/Tween/Tag와 같은 판단 재사용 — `quad-roblox` 코어에 직접
|
||||
포함, 별도 opt-out 패키지로 안 쪼갬.
|
||||
|
||||
## 열린 질문 (`.claude/question.md`에도 취합)
|
||||
|
||||
- 타입 파라미터화 이름(`Attribute<T>` 제네릭 vs `BooleanAttribute`류 정적
|
||||
패밀리 vs 절충) — 위 "문제" 절 참고, 다음 세션 사용자 판단 필요.
|
||||
- **이름은 잠정 확정, 최종 확정은 대기열**: 겹침 방지를 위해 그룹 값은
|
||||
`Attribute`, 단일 키는 `AttributeKey<<T>>`로 코드/문서 전체 통일해서
|
||||
당장의 해석 모호성은 없앴음 — 그래도 최종 이름은 다른 가칭들(`State`/
|
||||
`DI`→`D`/`Slot`/`canExecute`/`Brand`)과 함께 `.claude/question.md` 용어정리
|
||||
대기열에 있음, 나중에 한꺼번에 재검토.
|
||||
|
|
|
|||
|
|
@ -2224,10 +2224,13 @@ DI 키로 확정(2026-08-10 세션).** 이벤트는 `inst[key]`가 이미 Signal
|
|||
## Tag/Attribute 특수 키 — 전용 문서로 분리됨 (2026-08-07 여덟 번째 세션)
|
||||
|
||||
`base/tag-plan.md`/`base/attribute-plan.md`로 이동 — 이 절이 다루던 타입
|
||||
파라미터화 문제(`[Attribute<<boolean>> "name"]` vs `[BooleanAttribute
|
||||
"name"]`)뿐 아니라 `None`/`process`/`retract` 동작까지 확정 반영됨.
|
||||
UICorner 숏핸드/Tween처럼 "1 프리미티브 1 파일" 관례를 따라야 한다는
|
||||
지적으로 분리.
|
||||
파라미터화 문제(`[AttributeKey<<boolean>> "name"]`(구 `Attribute<<boolean>>`)
|
||||
vs `[BooleanAttribute "name"]`)뿐 아니라 `None`/`process`/`retract` 동작까지
|
||||
확정 반영됨. UICorner 숏핸드/Tween처럼 "1 프리미티브 1 파일" 관례를 따라야
|
||||
한다는 지적으로 분리. **[2026-08-11 아홉 번째 세션]** `attribute-plan.md`에
|
||||
여러 Store를 한 번에 attribute로 묶는 그룹 `Attribute(...)` 프리미티브(`Tag`와
|
||||
동형)가 추가되며, 단일 키 생성자는 이름 충돌 방지로 `AttributeKey<<T>>`로
|
||||
리네임됨.
|
||||
|
||||
## `Brand` — 런타임 nominal 타입 판별 통합 메커니즘, `isState`를 일반화 (2026-08-07 여덟 번째 세션)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
# `OnChange` 특수 키 — `GetPropertyChangedSignal` 바인딩
|
||||
|
||||
**상태**: base — 2026-08-10 세션에서 확정. quad-roblox 전용(값 타입/API
|
||||
레이어 없음, `Attribute`와 같은 패키지 배치).
|
||||
레이어 없음, `Attribute`와 같은 패키지 배치). **[2026-08-11 아홉 번째
|
||||
세션 후속]** `AttributeKey`와 동일한 이름별 weak 캐시로
|
||||
`OnChange(a) == OnChange(a)` 동등성도 확정 — 아래 "확정" 절 참고.
|
||||
|
||||
## 문제
|
||||
|
||||
|
|
@ -16,7 +18,10 @@
|
|||
## 확정
|
||||
|
||||
- **`OnChange(propertyName): OnChangeKey`** — 프로퍼티 이름을 감싸는 DI 키
|
||||
팩토리, `Attribute(name)`/`Tag(...)`와 같은 패턴. 사용 예:
|
||||
팩토리, `AttributeKey(name)`/`Tag(...)`와 같은 패턴(`AttributeKey`는 구
|
||||
`Attribute` — 2026-08-11 아홉 번째 세션에 여러 Store를 묶는 그룹
|
||||
`Attribute(...)` 프리미티브가 신설되며 이름 충돌 방지로 리네임됨,
|
||||
`base/attribute-plan.md` 참고). 사용 예:
|
||||
`Frame { [OnChange "Position"] = function(v: UDim2) ... end }`.
|
||||
- **제네릭 타입 파라미터 없음 — `OnChange<<T>>` 같은 타입 파라미터화는 안
|
||||
함.** 콜백 파라미터 타입은 호출부가 인라인으로 직접 명시
|
||||
|
|
@ -25,15 +30,17 @@
|
|||
Luau가 검증 못 하는 대가를 받아들인다"는 결정(`bind-system-plan.md` "이벤트
|
||||
바인딩 — `On.EventName` 도트액세스 안 씀" 절, "타입 안전성을 어느 정도
|
||||
포기하는 대가")과 같은 급의 트레이드오프라 새로 정당화할 것 없음 — 오히려
|
||||
`Attribute<<T>>`처럼 제네릭으로 정확히 맞추려는 시도는 이벤트 키보다 더
|
||||
`AttributeKey<<T>>`처럼 제네릭으로 정확히 맞추려는 시도는 이벤트 키보다 더
|
||||
엄격한 걸 요구하는 셈이라 일관성이 깨짐.
|
||||
- **기각안 — 프로퍼티별 정적 `OnChange.PropertyName` 전량 코드 생성**:
|
||||
`archive/onchange-per-property-codegen-rejected.md` 참고. Attribute의
|
||||
"제네릭 + 자주 쓰는 것만 정적 지름길" 절충과 겉보기엔 비슷해 보이지만
|
||||
규모가 다른 문제라 기각.
|
||||
- **패키지 경계: 전부 quad-roblox** — `Handlers/OnChange.luau`에 `OnChange(name)`
|
||||
키 팩토리와 Handler를 같이 둠(`Attribute.luau`와 같은 배치, base 쪽 값
|
||||
타입 파일 없음). `GetPropertyChangedSignal` 자체가 Roblox 엔진 API라 base에
|
||||
키 팩토리와 Handler를 같이 둠(단일 키 `AttributeKey.luau`와 같은 배치,
|
||||
base 쪽 값 타입 파일 없음 — 그룹 `Attribute.luau`는 값 타입 자체가
|
||||
quad-base 소속이라 다름, `base/attribute-plan.md` 참고).
|
||||
`GetPropertyChangedSignal` 자체가 Roblox 엔진 API라 base에
|
||||
둘 이유가 없음 — Tag처럼 백엔드 무관한 값/API 레이어가 따로 있는 경우와
|
||||
다름.
|
||||
- **`process(inst,k,v)`**: `inst:GetPropertyChangedSignal(name):Connect(function()
|
||||
|
|
@ -46,13 +53,22 @@
|
|||
구현하면 되고, `v`가 State/Source면 범용 `Dispatch/StoreBind.luau`가 알아서
|
||||
언랩+재귀 재-dispatch해서 `process`를 다시 호출해줌. `OnChange` 전용 분기
|
||||
불필요.
|
||||
- **`OnChange(name)`도 `AttributeKey`와 같은 이름별 weak 캐시 적용
|
||||
(2026-08-11 아홉 번째 세션 후속)** — `AttributeKey<<T>>(name)`이
|
||||
이름만으로 캐시되는 것과 정확히 같은 모양(이름 → 키, 다른 가변 정보
|
||||
없음)이라 같은 기법 그대로 재사용(`base/attribute-plan.md` "동등성"
|
||||
절). `State<function>`이 되더라도 문제 없이 작동하고(캐시는 키
|
||||
객체 자체의 identity만 다루지, 그 키에 바인딩된 값/콜백과는 무관),
|
||||
`OnChange "a" == OnChange "a"`가 외부에서 관찰 가능해지는 것도
|
||||
의도적으로 허용해도 되는 동작 — 문제 없음(사용자 확인). `Handlers/OnChange.luau`
|
||||
안 `OnChange(name)` 팩토리에 `AttributeKey`와 동일한 캐시 구현.
|
||||
|
||||
## 다른 특수 DI 키와의 대조
|
||||
|
||||
| | 소스 | 값 타입 | 패키지 경계 |
|
||||
|---|---|---|---|
|
||||
| 이벤트(`MouseButton1Click = fn`) | `inst[key]`가 이미 Signal | 콜백, 타입 미검증 | quad-roblox(`Handlers/Event.luau`) |
|
||||
| `Attribute(name)` | `SetAttribute`/`GetAttribute` | 값(제네릭 또는 정적 타입 패밀리로 타입 파라미터화) | quad-roblox(`Handlers/Attribute.luau`) |
|
||||
| `AttributeKey(name)` | `SetAttribute`/`GetAttribute` | 값(제네릭 또는 정적 타입 패밀리로 타입 파라미터화) | quad-roblox(`Handlers/AttributeKey.luau`) |
|
||||
| `OnChange(name)` | `GetPropertyChangedSignal(name)` | 콜백, 타입 미검증(제네릭 없음) | quad-roblox(`Handlers/OnChange.luau`) |
|
||||
|
||||
`OnChange`가 Attribute처럼 제네릭화되지 않은 이유는 "콜백을 받는다"는
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ Roblox Instance 이름과 맞춘 `UICorner`/`UIPadding`(+`UIPaddingOffset`)/
|
|||
이미 예시로 든 `mod:UICorner(8)`은 이 특수 키를 flatten해서 props에
|
||||
꽂아넣는 사탕 문법일 뿐, 실제 처리는 이 Handler가 함 — Modifier를 안 거치고
|
||||
`Frame { UICorner = 8 }`처럼 순수 인라인 키로 직접 써도(v1처럼) 동일하게
|
||||
작동함, `architecture.md`의 `[Attribute "Name"]`류 특수 키와 같은 층위.
|
||||
작동함, `architecture.md`의 `[AttributeKey "Name"]`류 특수 키와 같은 층위.
|
||||
자동 생성된 자식은 기존 관례대로 `_`/`QUAD_` 접두어 네이밍
|
||||
(`research/debug-tooling-plan.md` 9번, v1의 `_quad_round`류 그대로 재사용).
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
--!strict
|
||||
--[[
|
||||
검증 대상: `[Attribute<<boolean>> "name"] = value`처럼 제네릭 파라미터로
|
||||
검증 대상: `[AttributeKey<<boolean>> "name"] = value`처럼 제네릭 파라미터로
|
||||
타입을 명시하는 특수 DI 키를 테이블 리터럴에 쓸 때, `=` 뒤 `value`의
|
||||
타입이 실제로 그 제네릭 파라미터로 좁혀지는지 — Luau 타입 솔버가
|
||||
"이 계산된 키의 제네릭 인스턴스에 따라 옆 값의 타입이 달라진다"는
|
||||
|
|
@ -23,10 +23,10 @@
|
|||
그대로 확인 가능함.
|
||||
]]
|
||||
|
||||
-- SpecialKey<T> — Attribute<<T>>(name)이 반환하는 "타입이 실린 키" 흉내
|
||||
-- SpecialKey<T> — AttributeKey<<T>>(name)이 반환하는 "타입이 실린 키" 흉내
|
||||
type SpecialKey<T> = { __attributeKeyBrand: T }
|
||||
|
||||
local function Attribute<T>(name: string): SpecialKey<T>
|
||||
local function AttributeKey<T>(name: string): SpecialKey<T>
|
||||
return (nil :: any) :: SpecialKey<T>
|
||||
end
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ type HomogeneousParams = {
|
|||
}
|
||||
|
||||
local homo: HomogeneousParams = {
|
||||
[Attribute("Enabled")] = true, -- 이건 당연히 통과해야 함
|
||||
[AttributeKey("Enabled")] = true, -- 이건 당연히 통과해야 함
|
||||
}
|
||||
|
||||
-- ===== 시도 2: 이질적(heterogeneous) — 한 테이블에 boolean/number Attribute를 섞음 =====
|
||||
|
|
@ -50,11 +50,11 @@ local homo: HomogeneousParams = {
|
|||
-- 뭉개짐, (c) 테이블 타입 자체를 선언하는 시점에 에러)
|
||||
|
||||
local mixedProps: { [SpecialKey<any>]: any } = {} -- 일단 any로 도피한 버전(항상 통과할 것)
|
||||
mixedProps[Attribute("Enabled")] = true
|
||||
mixedProps[Attribute("Count")] = 5
|
||||
mixedProps[AttributeKey("Enabled")] = true
|
||||
mixedProps[AttributeKey("Count")] = 5
|
||||
|
||||
-- 진짜 물어볼 질문: 개별 대입 표현식 하나만 놓고 봤을 때, Luau가
|
||||
-- `Attribute<T>(name)`의 제네릭 인스턴스화 결과로 옆의 값 리터럴 타입을
|
||||
-- `AttributeKey<T>(name)`의 제네릭 인스턴스화 결과로 옆의 값 리터럴 타입을
|
||||
-- 체크/추론해주는지 — 함수 호출 결과 타입과 그 옆 대입값 사이의 관계는
|
||||
-- "인덱스 시그니처"가 아니라 그냥 "함수 반환 타입에 맞는 변수 대입"
|
||||
-- 문제로 좁혀서 아래처럼 직접 테스트:
|
||||
|
|
@ -64,12 +64,12 @@ local function setAttributeTyped<T>(key: SpecialKey<T>, value: T)
|
|||
-- 이 스파이크는 타입 추론 자체만 봄
|
||||
end
|
||||
|
||||
setAttributeTyped(Attribute("Enabled"), true) -- T=boolean으로 추론돼 통과해야 함
|
||||
setAttributeTyped(Attribute("Count"), 5) -- T=number로 추론돼 통과해야 함
|
||||
setAttributeTyped(Attribute("Enabled"), 5) -- <- 여기가 핵심: T=boolean인데 5(number)를 넘김.
|
||||
setAttributeTyped(AttributeKey("Enabled"), true) -- T=boolean으로 추론돼 통과해야 함
|
||||
setAttributeTyped(AttributeKey("Count"), 5) -- T=number로 추론돼 통과해야 함
|
||||
setAttributeTyped(AttributeKey("Enabled"), 5) -- <- 여기가 핵심: T=boolean인데 5(number)를 넘김.
|
||||
-- 이게 타입 에러로 잡히면(기대하는 결과) "제네릭 키 함수 호출 패턴"은
|
||||
-- 최소한 함수 인자 형태로는 잘 작동한다는 뜻 — 그럼 테이블 리터럴
|
||||
-- `{[Attribute<<T>>(name)] = value}` 안에서도 Luau가 "이건 사실
|
||||
-- `{[AttributeKey<<T>>(name)] = value}` 안에서도 Luau가 "이건 사실
|
||||
-- 위 setAttributeTyped 호출과 같은 형태"로 취급해주는지가 다음 질문.
|
||||
|
||||
print("런타임 실행은 의미 없음 — luau-analyze/luau-lsp 진단만 확인할 것")
|
||||
|
|
@ -80,7 +80,7 @@ print(homo, mixedProps)
|
|||
1. `setAttributeTyped(Attribute("Enabled"), 5)` 줄에서 실제로 타입
|
||||
에러가 나는가? — 나면 "함수 인자 형태의 제네릭 키+값 연동"은
|
||||
Luau가 지원한다는 뜻.
|
||||
2. 위가 통과한다면, 그 다음으로 `mixedProps[Attribute("Enabled")] =
|
||||
2. 위가 통과한다면, 그 다음으로 `mixedProps[AttributeKey("Enabled")] =
|
||||
5`처럼 **인덱스 대입 문법**으로도 같은 체크가 되는지 직접 추가해
|
||||
실험해볼 것(이 파일엔 일부러 안 넣어둠 — `{[SpecialKey<T>]: T}`류
|
||||
제네릭 인덱스 시그니처를 실제로 선언할 수 있는지부터 luau-lsp가
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해
|
|||
| `09-type-modifier-overridden-subtype.luau` (타입체크 전용) | `FrameModifier <: GuiObjectModifier`처럼 서브타입 관계인 Modifier를 `Overridden`으로 섞을 때 타입이 통과하는지 | `modifier-plan.md` 9-2번, ROADMAP M7 |
|
||||
| `10-roblox-studio-checks.server.luau` (Studio 전용) | (A) `bindLifetime`/`unbindLifetime`/`canExecute`/`canBound`의 gcconn 트릭 + 이중 바인딩 게이트(Destroy 시 Connected 전환 포함), (B) Attribute의 Instance 참조 타입 지원, (C) CollectionService 태그/GetTagged 왕복 | `lifecycle-pattern.md`, `bind-system-plan.md` "이중 바인딩 금지", CLAUDE.md 2026-08-06 세션, `debug-tooling-plan.md` |
|
||||
| `11-modifier-illegal-value-error.luau` | Modifier 필드에 Ref/PreRef/Observer/Effect/Slot/Modifier가 들어오면 즉시 error, State/Source가 확정하는 값이 Modifier면 즉시 error(2026-08-09 세션에 "UB"에서 전환된 규칙) | `modifier-plan.md` "핸들러 계층 값 즉시 error" 절 + 7번 절 |
|
||||
| `12-type-attribute-generic-key-narrowing.luau` (타입체크 전용) | `[Attribute<<T>> "name"] = value`처럼 제네릭 DI 키를 쓸 때 `value`의 타입이 실제로 `T`로 좁혀지는지 — base 문서 자신이 "미검증"이라 명시한 항목 | `attribute-plan.md` "[실측 필요, M0/M10]" (2026-08-09 열한 번째 세션 신설) |
|
||||
| `12-type-attribute-generic-key-narrowing.luau` (타입체크 전용) | `[AttributeKey<<T>> "name"] = value`(구 `Attribute<<T>>`)처럼 제네릭 DI 키를 쓸 때 `value`의 타입이 실제로 `T`로 좁혀지는지 — base 문서 자신이 "미검증"이라 명시한 항목 | `attribute-plan.md` "[실측 필요, M0/M10]" (2026-08-09 열한 번째 세션 신설) |
|
||||
| `13-type-ref-preref-subtype.luau` | (A, 타입) `PreRef<T>`가 `Ref<T>`를 구조적으로 만족하는지, (B, 런타임) `isRef`/`isPreRef` 합성이 재정정대로 동작하는지(`isRef(preRefInstance)`가 이제 `true`) + Leaf 핸들러가 `isRef(v) and not isPreRef(v)`로 명시적으로 좁혀야 하는 이유 | `bind-system-plan.md`의 `Brand` 절(2026-08-09 열한 번째 세션 재정정) |
|
||||
| `14-type-nilable-default-overload.luau` (타입체크 전용) | `Source(default)`/`Ref(default)`의 `default` 생략이 `T`가 nilable일 때만 안전하다는 캐비엇을, 함수 오버로드(교차 타입)로 실제로 타입 레벨에서 막을 수 있는지 | `bind-system-plan.md` "[보강, 2026-08-09 열한 번째 세션]" 절 |
|
||||
| `15-type-compute-trailing-deps-typepack.luau` (타입체크 전용) | `:Compute(fn, ...)`의 trailing deps를 `fn`에 위치 인자(lazy State 핸들)로도 노출하는 확장, 최종 시그니처 `fn(self, previous?, ...deps)` — 이형(heterogeneous) 다중 deps를 제네릭 타입 팩(`U...`)으로 표현 가능한지, `previous?`가 팩 앞(정정된 순서)에서만 통과하고 팩 뒤(옛 순서)에서는 막히는지 | `bind-system-plan.md` "trailing deps를 fn에 lazy positional 인자로도 노출" 절(2026-08-11 후속 세션, 순서는 같은 날 세 번째 세션에 정정) |
|
||||
|
|
|
|||
|
|
@ -138,6 +138,15 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
|
|||
"`Tag`가 이미 이 뜻으로 쓰이고 있어서 충돌"이라는 이유로 `Brand`의
|
||||
대안 이름 후보에서 제외됐다는 점은 참고할 것 — 두 이름이 같은 코퍼스
|
||||
안에서 공존 가능한지도 같이 검토 대상.
|
||||
- **`Attribute`/`AttributeKey`(3순위, 사소함, 2026-08-11 아홉 번째 세션
|
||||
추가)**: 여러 Store를 한 번에 attribute로 묶는 그룹 프리미티브
|
||||
(`Attribute(store1, store2, ...)`, `Tag`와 동형)가 신설되면서, 기존
|
||||
단일 키 생성자 `Attribute<<T>>("name")`를 이름 충돌 방지를 위해
|
||||
`AttributeKey<<T>>`로 잠정 리네임함(`OnChange`/`OnChangeKey`처럼 함수
|
||||
이름과 반환 타입 이름이 분리된 기존 전례와 대칭) — 해석 모호성 자체는
|
||||
이미 없앴으니 급하지 않지만, 최종 이름은 여전히 이 목록의 다른
|
||||
가칭들과 함께 검토 대상. `base/attribute-plan.md` "그룹 `Attribute(...)`"
|
||||
절 참고.
|
||||
|
||||
### 2. 구현 착수 직전 감사 결과 (2026-08-06 신설, M0 착수 전 확인 권장)
|
||||
|
||||
|
|
@ -228,11 +237,12 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
|
|||
패턴)** — `research/documentation-plan.md`(뼈대만). 정식 백로그 항목으로
|
||||
올릴지, 착수 시점을 언제로 볼지 사용자 판단 필요.
|
||||
- **[해소됨, 2026-08-09 열한 번째 세션]** Attribute 특수 키 타입
|
||||
파라미터화 — `[Attribute<<boolean>> "name"]` 제네릭 스타일과
|
||||
`[BooleanAttribute "name"]` 타입별 정적 생성자 패밀리 **둘 다 채택으로
|
||||
확정**(내부 구현 동일, 호출부 표기만 다름). `base/attribute-plan.md`
|
||||
참고 — 제네릭 파라미터가 `=` 뒤 값 타입까지 좁혀주는지는 M0/M10에서
|
||||
실측 필요(안 돼도 런타임엔 영향 없음).
|
||||
파라미터화 — `[AttributeKey<<boolean>> "name"]`(구 `Attribute<<boolean>>`,
|
||||
2026-08-11 아홉 번째 세션에 그룹 `Attribute(...)`와의 이름 충돌 방지로
|
||||
리네임) 제네릭 스타일과 `[BooleanAttribute "name"]` 타입별 정적 생성자
|
||||
패밀리 **둘 다 채택으로 확정**(내부 구현 동일, 호출부 표기만 다름).
|
||||
`base/attribute-plan.md` 참고 — 제네릭 파라미터가 `=` 뒤 값 타입까지
|
||||
좁혀주는지는 M0/M10에서 실측 필요(안 돼도 런타임엔 영향 없음).
|
||||
- **v1 하위호환(compat) 레이어 — `quad-roblox-v1-compat`** —
|
||||
`research/v1-compat-plan.md`(신규, 2026-08-06, 두 차례 후속 논의로 수렴).
|
||||
방향 확정: v1을 그대로 병행 실행 + 경계에서만 `state:Observer()`(lazy
|
||||
|
|
|
|||
140
.claude/session/2026-08-11-09-attribute-group-primitive.md
Normal file
140
.claude/session/2026-08-11-09-attribute-group-primitive.md
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
<!-- quad-v2 세션 로그 원문. -->
|
||||
<!-- 이 파일은 quadnomicon 개발로그 소재용 원자료로, 당시 논의(시행착오 포함)를 그대로 보존함. -->
|
||||
<!-- 현재 유효한 설계는 이 파일이 아니라 base//research//archive/가 최종 소스 — 이 파일 안의 판단이 이후 세션에서 뒤집혔을 수 있음. -->
|
||||
|
||||
## 2026-08-11 아홉 번째 세션 — 그룹 `Attribute(...)` 프리미티브 신설, 단일 키
|
||||
`AttributeKey`로 리네임
|
||||
|
||||
사용자가 "attribute를 store로 설정 가능하게 두면 어떨까"라는 아이디어를
|
||||
자유 서술 메모로 제기하며 시작. 이미 확정된 단일 키
|
||||
`[Attribute<<T>> "name"] = value`(`base/attribute-plan.md`) 모델은 필드
|
||||
하나하나를 개별 DI 키로 나열해야 해서, Store 전체를 한 번에 attribute로
|
||||
투영하고 싶은 요구를 못 채움 — 사용자가 스스로 네 가지 형태
|
||||
(`[Attribute] = Store{...}` 해시파트 단일슬롯 / `Attribute`를 Store
|
||||
서브타입으로 / `Attribute(Store)` 프리미티브 / Slot처럼 제공)를 트레이드오프와
|
||||
함께 나열하고 의견을 구함.
|
||||
|
||||
### 1라운드 — 형태 결정
|
||||
|
||||
- **`[Attribute] = Store {...}` 해시파트 단일슬롯 기각**: 인스턴스당
|
||||
슬롯이 하나뿐이라, 헤테로지니어스 Store 여러 개(스타일 Store + 상태
|
||||
Store 등)를 합칠 방법이 구조적으로 없음.
|
||||
- **Attribute가 Store를 상속(IS-A) — 기각**: `Store<T>`의 `T`가 다시
|
||||
Attribute(=Store)일 수 있게 되어, 이미 확정된 "핸들러 계층 값은
|
||||
Source에 못 들어감"(`Store<T>`의 `T`는 Modifier 불가와 같은 이유) 제약과
|
||||
부딪히는 "Store 안에 Store"를 실제로 만들어냄.
|
||||
- **채택 — `Tag`와 동형인 array-part 값 객체**: `Attribute(store1, store2,
|
||||
..., {plain=1})` 생성자 + `Attribute.Merged(a,b,...)`. 여러 개를 그냥
|
||||
나란히 놔도(`Frame { Attribute(a), Attribute(b) }`) 각자 자기 키만
|
||||
반영하므로 헤테로지니어스 합성 문제가 `Tag`처럼 자연히 풀림.
|
||||
|
||||
### 2라운드 — 메커니즘: AttributeChanged 남발 방지, 자기 완결형 Handler
|
||||
|
||||
사용자가 직접 지적: `State<Attribute>`(그룹 값 자체가 `:Compute`로
|
||||
스왑되는 경우)까지 오면, 재처리 때마다 전부 지웠다 다시 set하면
|
||||
`GetAttributeChangedSignal` 남발. 해법으로 사용자가 `Tag`의 diff 패턴을
|
||||
그대로 재사용하자고 제안 — `(inst, index)` 릴레이션에 이전 키 이름 집합을
|
||||
저장해두고, 사라진 키만 지우고 남은/새 키는 값 비교 없이 그냥 다 set(값
|
||||
비교까지 하려면 `:Get()`이 필요한데 그 부작용을 감수할 이유가 없다고
|
||||
판단, "단순히 nil 했다 새 값으로 덮는다만 줄이기").
|
||||
|
||||
이어서 이게 "그룹 재처리"에서만 의미 있고, 필드 하나만 바뀌는 흔한
|
||||
경우(그룹 자체는 안 바뀜)는 그 키 전용 구독이 직접 `SetAttribute` 호출로
|
||||
끝나 이 diff 경로를 아예 안 거친다는 점을 정리(어시스턴트).
|
||||
|
||||
추가로 어시스턴트가 제안: 내부에서 단일 키 `AttributeKey<<T>>(name)` DI
|
||||
키 객체를 재합성해서 Dispatch에 재진입시키면, 매 처리마다 새로 만들어지는
|
||||
키 객체의 참조 동등성 때문에 Dispatch의 `(inst,k)`별 핸들러 체인 추적이
|
||||
깨질 위험 — 그룹 핸들러는 `TagHandler`처럼 자기 완결형으로(직접
|
||||
`SetAttribute` 호출 + `Dispatch/StoreBind`로 개별 필드 구독) 가는 게
|
||||
낫다고 제안. 사용자 확인: "내부적으로 SetAttribute 실행 하나뿐이라 자기
|
||||
완결이 아주 쉬움, 그걸 위해 모듈화 할 것도 없고" — 동의, 별도 재진입
|
||||
경로 자체를 안 만드는 걸로 확정.
|
||||
|
||||
retract는 Tag와 동일하게 자기가 쓴 키 전부 `SetAttribute(name, nil)`로
|
||||
정리하는 걸로 확정(어시스턴트 제안, "소진돼도 방치해도 부작용 없다"는
|
||||
사용자의 최초 메모와는 다른 방향 — Roblox Instance 풀링/재사용 시 이전
|
||||
값이 새는 게 실제 버그라는 근거로 제시, 사용자 동의: "5는 네 말이
|
||||
맞아보여").
|
||||
|
||||
### 3라운드 — `Attribute.Merged`가 "레이어드 Store 기각"과 충돌 안 하는지
|
||||
|
||||
사용자가 스스로 짚음: `Attribute.Merged`가 여러 Store의 Source 슬롯을
|
||||
가져와 자기 맵에 넣는 구조인데, 예전에 `Context` 대안으로 나왔던 "레이어드
|
||||
Store"(`archive/context-rejected.md`)를 기각한 적이 있어서 이것도 같은
|
||||
문제 아닌지. 사용자 자체 결론: "값을 넣기 위한 것이 레이어된다 보긴
|
||||
애매하고, 목적성이 있는 객체의 레이어라서 문제는 안 보임" — 이건 `Modifier`의
|
||||
`Overridden`과 유사한 사례라는 판단. 어시스턴트가 검증: 기각됐던 레이어드
|
||||
Store는 **읽는 시점의 암묵적 부모 체인 자동 폴백**(디버깅 불투명성)이
|
||||
문제였지, `Attribute.Merged`는 **작성 시점에 명시적으로 한 번** Source
|
||||
참조를 평탄한 맵으로 모으는 것뿐이라 그 기각 사유(범용 컨테이너의 암묵
|
||||
런타임 폴백)가 적용 안 됨 — 확인 완료.
|
||||
|
||||
### 4라운드 — 이름 충돌: 단일 키를 `AttributeKey`로 리네임
|
||||
|
||||
기존 단일 키 생성자 이름이 `Attribute<<T>>`였는데, 그룹 프리미티브도
|
||||
`Attribute(...)`라는 같은 이름을 쓰면 "Store 받는 애인지 단일 키인지"
|
||||
해석이 갈림. 사용자가 명시: "지금 그대로 두면 Attribute가 스토어 받는
|
||||
애인지 단일키인지 해석 여지가 갈리니, 임시 네이밍으로 AttributeKey로
|
||||
둬야할듯" — `OnChange`/`OnChangeKey`(함수 이름과 반환 타입 이름이 분리된
|
||||
기존 전례)와 대칭되는 이름이라 어시스턴트도 동의. **다른 용어 정리
|
||||
항목들과 달리 이건 "임시"가 아니라 지금 바로 코드/문서 전체에 적용** —
|
||||
최종 이름 확정만 대기열(`question.md`)에 남김, 표기 자체의 모호성은
|
||||
지금 없앰. `[BooleanAttribute "name"]`류 정적 패밀리는 `Attribute`라는
|
||||
이름을 쓰지 않으므로 리네임 대상 아님.
|
||||
|
||||
### 5라운드 — 이름별 weak 캐시로 동등성 보장, 메커니즘 재개정
|
||||
|
||||
문서 반영 직후 사용자가 새 아이디어 제기: `AttributeKey "aaa"`가 GC
|
||||
전까지 이름별 weak 캐시로 항상 같은 객체를 리턴하게 만들면 어떤가 —
|
||||
구현은 `cache[name]` 조회 후 없으면 생성+저장하는 단순한 엔지니어링,
|
||||
비용도 크지 않음. 이러면 `AttributeKey "aaa" == AttributeKey "aaa"`가
|
||||
보장되고, 사용 중엔 항상 같은 게 리턴됨. 사용자 스스로 `Tag`와 대조:
|
||||
Tag는 내부 이름 목록 자체가 매번 달라지는 게 핵심이라 동등성 비교가
|
||||
의미 없지만, `AttributeKey`는 이름 외 다른 가변 정보가 없는 순수 매핑이라
|
||||
캐싱/동등성 비교가 말이 됨.
|
||||
|
||||
**확정**: `AttributeKey(name)`(및 `BooleanAttribute` 등 정적 패밀리 —
|
||||
캐시 키는 순수 문자열 `name`뿐, 제네릭 `T`는 런타임 무영향이라 무시)이
|
||||
값만 weak인 캐시(`setmetatable(cache, {__mode="v"})`)를 거치도록 확정.
|
||||
GC 타이밍: 뭔가(Dispatch의 체인 저장 등)가 강하게 붙들고 있는 동안은
|
||||
캐시도 살아있어 동일 이름 재호출 시 항상 같은 객체, 아무도 안 붙들면
|
||||
자연히 풀리는데 그 시점엔 이전 identity를 참조하는 곳이 없어 문제 없음.
|
||||
|
||||
**이게 4라운드에서 정한 "그룹 Handler는 자기 완결형, Dispatch 재진입
|
||||
없음" 결정 자체를 뒤집음** — 그 결정의 근거였던 "매번 새 키 객체라
|
||||
Dispatch의 (inst,k) 체인 추적이 깨질 위험"이 캐시로 사라지므로,
|
||||
어시스턴트가 메커니즘을 재개정: 그룹 Handler는 이제 자기만의
|
||||
`SetAttribute`/구독 로직을 만들지 않고, 메모이즈된 `AttributeKey(name)`로
|
||||
`Dispatch.process`/`Dispatch.retractUnder`에 각 필드를 그대로 재귀
|
||||
위임 — 작성자가 직접 `[AttributeKey<<T>> name] = source`를 쓴 것과
|
||||
완전히 같은 경로를 태워 단일 키의 `None`/`retract`/store-bind 로직을
|
||||
100% 재사용(중복 구현 제거). 그룹 Handler에 남는 자기 로직은 사실상
|
||||
"이전 이름 집합과의 diff"뿐. `OnChangeKey`도 같은 모양(이름→키, 다른
|
||||
가변 정보 없음)이라 같은 기법이 그대로 적용 가능하다는 점도 기록(급하지
|
||||
않음, 지금 뭘 풀어주는 캐치는 아직 없음).
|
||||
|
||||
### 6라운드 — `OnChangeKey`에도 같은 캐시 적용
|
||||
|
||||
5라운드 문서화에서 "OnChangeKey도 같은 기법 적용 가능하나 급하지 않음"으로
|
||||
남겨뒀던 걸 사용자가 바로 확정지음: `OnChange`가 `State<function>`이
|
||||
되더라도(캐시는 키 객체 identity만 다루지 바인딩된 콜백/값과는 무관이라)
|
||||
문제없이 작동할 것이고, `OnChangeKey "a" == OnChangeKey "a"`가 외부에
|
||||
관찰 가능해지는 것도 의도적으로 허용해도 되는 동작이라 문제 없다고 직접
|
||||
판단 — 같이 반영. `base/onchange-plan.md`에 `AttributeKey`와 동일한
|
||||
이름별 weak 캐시 적용을 확정 절로 추가.
|
||||
|
||||
### 반영된 파일
|
||||
|
||||
`base/attribute-plan.md`(그룹 `Attribute(...)` 절 신규, 단일 키 전체
|
||||
`AttributeKey`로 리네임)/`base/architecture.md`(소스 트리에 base
|
||||
`Attribute.luau` + roblox `AttributeKey.luau`/`Attribute.luau` 추가,
|
||||
4번 항목 갱신)/`base/onchange-plan.md`/`base/bind-system-plan.md`/
|
||||
`base/ui-shorthand-plan.md`(전부 `Attribute<<T>>` 언급을 `AttributeKey<<T>>`로
|
||||
치환)/`.claude/README.md`/`.claude/question.md`(용어정리 대기열에
|
||||
`Attribute`/`AttributeKey` 항목 추가, 기존 "해소됨" 타입파라미터화
|
||||
항목에도 리네임 각주).
|
||||
|
||||
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, `luau-test` 결과
|
||||
확인 우선) — 이번 세션도 순수 설계 확정이라 M0 착수 우선순위 자체는
|
||||
그대로.
|
||||
12
CLAUDE.md
12
CLAUDE.md
|
|
@ -410,3 +410,15 @@ CLAUDE.md가 세션 로그 누적으로 3196줄까지 불어나 컨텍스트 성
|
|||
해왔음을 `README.md`/`question.md` 대조로 확인(반영 누락 없음 — archive
|
||||
이전 전 처리 불필요). 새 서사가 CLAUDE.md에 다시 쌓이지 않도록 "이 절을
|
||||
갱신하는 방법" 절을 세션 히스토리 맨 위에 명문화.
|
||||
|
||||
**2026-08-11 아홉 번째 세션 — 그룹 `Attribute(...)` 프리미티브, 단일 키 `AttributeKey`로 리네임, 이름별 weak 캐시**
|
||||
(`session/2026-08-11-09-attribute-group-primitive.md`)
|
||||
여러 Store를 한 번에 attribute로 묶는 `Attribute(store1, store2, ...)`를
|
||||
`Tag`와 동형인 array-part 값 객체로 신설(`Merged`로 헤테로지니어스 합성,
|
||||
retract는 Tag처럼 확실히 청소). 이름 충돌 방지로 기존 단일 키
|
||||
`Attribute<<T>>`를 `AttributeKey<<T>>`로 즉시 리네임(용어정리 대기열이
|
||||
아니라 지금 바로 적용, 최종 이름만 대기열에 남김). **후속**: `AttributeKey(name)`이
|
||||
이름별 weak 캐시로 동등성(`AttributeKey(a) == AttributeKey(a)`)을 보장하도록
|
||||
확정되며, 최초안이던 "그룹 Handler 자기 완결형(Dispatch 재진입 없음)"을
|
||||
뒤집고 메모이즈된 키로 기존 단일 키 경로에 재귀 위임하는 걸로 재개정
|
||||
(중복 구현 제거) — `base/attribute-plan.md` 등 관련 문서 전체 반영 완료.
|
||||
|
|
|
|||
40
ROADMAP.md
40
ROADMAP.md
|
|
@ -80,17 +80,23 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
여덟 번째 세션 정정)
|
||||
- [ ] `Brand.luau`(공유 weak-key 레지스트리, `Brand.set(x,tag)`/
|
||||
`Brand.get(x)` — `isState`뿐 아니라 `isObserver`/`isEffect`/`isTag`/
|
||||
`isAttribute`/`isTween`/`isBlocker`/`isSource`/`isStore`/`isSlot`/
|
||||
`isRef`/`isPreRef`/`isModifier`(2026-08-07 열 번째 세션 추가 — 원래
|
||||
태그 목록에서 빠져있었음. **[정정, 2026-08-09 열한 번째 세션]**
|
||||
`isRef`/`isPreRef`는 `isState`처럼 상위-하위 관계로 재정정됨 —
|
||||
`isPreRef`가 가장 구체적인 항등, `isRef`는 그 위에 얹혀
|
||||
`isAttributeKey`/`isAttribute`/`isTween`/`isBlocker`/`isSource`/
|
||||
`isStore`/`isSlot`/`isRef`/`isPreRef`/`isModifier`(2026-08-07 열 번째
|
||||
세션 추가 — 원래 태그 목록에서 빠져있었음. **[정정, 2026-08-09
|
||||
열한 번째 세션]** `isRef`/`isPreRef`는 `isState`처럼 상위-하위 관계로
|
||||
재정정됨 — `isPreRef`가 가장 구체적인 항등, `isRef`는 그 위에 얹혀
|
||||
`isPreRef`도 `true`로 통과시킴(PreRef가 Ref 런타임을 재사용하는
|
||||
것과 정합). `(v=Ref)` children leaf 매치 핸들러는 이제
|
||||
`isRef(v) and not isPreRef(v)`로 명시적으로 좁혀야 함. `isModifier`는
|
||||
여전히 단순 항등, 상위 개념 없음) 전부의 기반. `isNone`만 예외로
|
||||
레지스트리 없이 `x == None` 항등 비교 — `bind-system-plan.md`의
|
||||
`Brand` 절, 2026-08-07 여덟 번째 세션 신설)
|
||||
여전히 단순 항등, 상위 개념 없음. **[정정, 2026-08-11 아홉 번째
|
||||
세션]** `isAttribute` 하나였던 게 `isAttributeKey`(단일 키 DI 키
|
||||
predicate, 해시파트 `k`를 판별)와 `isAttribute`(그룹 값 predicate,
|
||||
array-part `v`를 판별, `isTag`와 같은 결)로 분리됨 — 그룹
|
||||
`Attribute(...)` 프리미티브 신설로 같은 이름이 서로 다른 두
|
||||
대상(키 vs 값)을 가리키게 돼서 갈라짐, `base/attribute-plan.md`
|
||||
참고) 전부의 기반. `isNone`만 예외로 레지스트리 없이 `x == None`
|
||||
항등 비교 — `bind-system-plan.md`의 `Brand` 절, 2026-08-07 여덟
|
||||
번째 세션 신설)
|
||||
- [ ] `Relate.luau`(전체가 quad-base, 순수 Lua — `base/relate-plan.md`) —
|
||||
`Relate()` 비싱글톤 생성자, `:SetWeak`/`:GetWeak`/`:SetStrong`/`:GetStrong`.
|
||||
`inst`(첫 인자)는 항상 weak, `StrongMap`/`WeakMap` 서브테이블은 lazy
|
||||
|
|
@ -438,9 +444,21 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
|||
- [ ] `Handlers/Event.luau`(`ReflectionService` 기반 자동 판별)
|
||||
- [ ] `Handlers/OnChange.luau`(`OnChange(name)` DI 키 팩토리+Handler,
|
||||
`GetPropertyChangedSignal` 바인딩 — 제네릭 없이 콜백 타입은 인라인
|
||||
명시, `base/onchange-plan.md`, 2026-08-10 세션 확정)
|
||||
- [ ] `Handlers/Attribute.luau`(`base/attribute-plan.md` — 메커니즘/`None`/
|
||||
`retract` 불필요 확정, 타입 파라미터화 이름만 착수 전 확인)
|
||||
명시, 이름별 weak 캐시로 `OnChange(a) == OnChange(a)` 동등성 보장
|
||||
(`AttributeKey`와 동일 기법), `base/onchange-plan.md`, 2026-08-10
|
||||
세션 확정·2026-08-11 아홉 번째 세션 후속(캐시))
|
||||
- [ ] `Handlers/AttributeKey.luau`(단일 키 `AttributeKey<<T>>(name)`/
|
||||
`BooleanAttribute`류 DI 키 팩토리+Handler — 메커니즘/`None`/`retract`
|
||||
불필요 확정, 이름별 weak 캐시로 동등성 보장, 타입 파라미터화 이름만
|
||||
착수 전 확인, `base/attribute-plan.md`)
|
||||
- [ ] `Attribute.luau`(quad-base — 그룹 값 타입+API: `Attribute(store1,
|
||||
store2, ...)`/`Merged`, `Tag`와 동형 array-part 값 객체,
|
||||
`base/attribute-plan.md`)
|
||||
- [ ] `Handlers/Attribute.luau`(quad-roblox — 그룹 process/retract, 이전
|
||||
이름 집합과의 diff만 자체 로직, 실제 `SetAttribute`/store-bind
|
||||
구독은 메모이즈된 `AttributeKey(name)`로 `Dispatch.process`/
|
||||
`retractUnder`에 재귀 위임(단일 키 경로 재사용, 중복 구현 없음),
|
||||
`base/attribute-plan.md`)
|
||||
- [ ] `Tag.luau`(quad-base — 값 타입+immutable clone 체이닝: `Tag(...)`/
|
||||
`:Added`/`:Removed`/`:Contains`/`:Apply`/`Merged`, `base/tag-plan.md`
|
||||
— 2026-08-08 세 번째 세션 array-part 값 객체로 재설계, 구 해시 파트
|
||||
|
|
|
|||
Loading…
Reference in a new issue