diff --git a/.claude/README.md b/.claude/README.md index 67e8a52..4565277 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -56,7 +56,7 @@ | 문서 | 내용 | 우선순위 | |---|---|---| -| `tween-plan.md` | **[2026-08-12 세션에서 구조+옵션 값 모양+override 정책+`Animate` 콤비네이터까지 전부 확정]** 값-레벨 `Tween` 래퍼(PropertyHandler가 소비), 구 특수 bind key 모델은 `archive/tween-special-bind-key-reversed.md`로 이전. 3-상태 릴레이션 슬롯(`{Tween,Value}\|true\|nil`), `T'=T\|Tween` 타입 치환. 옵션 값 모양은 `Info: TweenInfo?` 우선+편의 필드(`Time`/`Style`/...) 폴백, override는 `Tween.Cancel`(기본)/`Tween.Finish` 2값. `Animate(info)`는 `Tween` opts(`Value` 제외)를 `T\|State`로 받아 `:Compute`에 바로 넘기는 sugar로 확정(구 `useTween` 스케치 대체). `initValue`는 사용자가 직접 처리(에이전트 범위 제외), 남은 건 자연완료 북키핑 하나뿐 | 하 — 사실상 다 닫힘 | +| `tween-plan.md` | **[2026-08-12 세션에서 구조+옵션 값 모양+override 정책+`Animate` 콤비네이터까지 전부 확정]** 값-레벨 `Tween` 래퍼(PropertyHandler가 소비), 구 특수 bind key 모델은 `archive/tween-special-bind-key-reversed.md`로 이전. 3-상태 릴레이션 슬롯(`{Tween,Value}\|true\|nil`), `T'=T\|Tween` 타입 치환. 옵션 값 모양은 `Info: TweenInfo?` 우선+편의 필드(`Time`/`Style`/...) 폴백, override는 `Tween.Cancel`(기본)/`Tween.Finish` 2값. `Animate(info)`는 `Tween` opts(`Value` 제외)를 `T\|State`로 받아 `factory(self)->State`를 반환하는 sugar로 확정(구 `useTween` 스케치 대체), 호출 경로는 같은 세션 후속 논의로 `:Compute`→`:Apply` 정정(재사용 가능한 콤비네이터 정합성 문제, `operator-sugar-plan.md`와 같은 근거). `initValue`는 사용자가 직접 처리(에이전트 범위 제외), 남은 건 자연완료 북키핑 하나뿐 | 하 — 사실상 다 닫힘 | | `existing-instance-bind-plan.md` | 이미 생성된 인스턴스 재바인드 — 착수 안 하되 "미지원" 확정도 안 함, 열린 가능성 유지 | 하 — v2 초기 스코프 제외 | | `debug-tooling-plan.md` | 실물 Instance→코드 위치 역추적 Studio 플러그인(`quad-debug`) — 채널 실현 가능성(BindableEvent/Function 크로스 컨텍스트)까지 실측 검증 완료, 세부 API 이름·구현만 남음 | 하 — 사용자가 "quad 개발 완료 전엔 착수 못 함"으로 직접 후순위 지정, base 설계 시 훅 확장 지점만 고려 | | `documentation-plan.md` | 문서 사이트 구조(초심자/api/심화/`quadnomicon` 4축, 백엔드별 트랙 분리) + UI 네이밍 컨벤션·Store 부작용 패턴·권장 이벤트 핸들링 3개 세부 문서 뼈대 | 하 — 착수 시점 미정, 구조/스코프만 합의된 상태 | @@ -64,6 +64,7 @@ | `framework-comparison-findings.md` | quad vs Fusion/Vide/react-lua 정직한 비교(실 소스 근거) — quad 강점, 진짜 불리한 점 중 고칠 만한 것 3개, 못 고치는 트레이드오프 정리 | 하 — 사용자 검토 후 반영 여부 결정 대기 | | `additional-primitives-plan.md` | **[2026-08-09 세 번째 세션, 전부 해소]** 마지막으로 남아있던 키 기반 동적 컬렉션 재조정도 `Slot:List(...)`로 확정되어 `base/slot-plan.md`로 승격 — 이 문서엔 새로 열린 설계 질문 없음, "빈 자리 아닌 것"/"문서화 백로그"/조사 소스 목록만 배경 자료로 유지 | 하 — 배경 리서치 기록용, 열린 결정 없음 | | `pre-implementation-audit.md` | M0 착수 직전 크리티컬 감사(2026-08-06 신설) — `base/` 전체를 모호성/지연결정리스크/단순화후보 세 렌즈로 재검토, 11개 우선순위1(M0~M4 착수 전 확인 권장) + 11개 우선순위2 + 2개 단순화후보 | 상 — M0 착수 전 최소 우선순위1 항목 확인 권장 | +| `operator-sugar-plan.md` | **[2026-08-12 신설]** `Sum`/`Product`/`Not`/비트연산 등 `:Compute`/`:Apply`용 연산자 콤비네이터 슈가 — 메커니즘은 이미 확정된 계약(`Animate`와 동형 패턴) 재사용이라 확정, 네임스페이스 이름만 미정 | 하 — 구현은 맨 마지막(순수 슈가, 없어도 무방, 함수 간 의존 없음), 사용자가 직접 후순위 지정 | | `v1-compat-plan.md` | v1 하위호환(compat) 레이어 — `quad-roblox-v1-compat` 패키지, v2→v1 단방향 브리지(`state:Observer()`+v1 프로퍼티 재대입), v2-in-v1/v1-in-v2 두 임베딩 방향의 기술 규칙까지 확정. quad2-try의 `quad-compat`은 빈 폴더로 실제 시도된 적 없었음을 확인 | 하 — Slot이 foreign Instance를 어떻게 다루는지만 Slot 코어 구현 시점까지 미결 | ## `archive/` — 완료됐거나 완전히 뒤집힌 것, 능동 참고 불필요 diff --git a/.claude/base/architecture.md b/.claude/base/architecture.md index 6d60f19..84313a2 100644 --- a/.claude/base/architecture.md +++ b/.claude/base/architecture.md @@ -170,7 +170,7 @@ quad/ │ ├── 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 {} }) - ├── Animate.luau # `Animate(condOrOpts, opts?)` 편의 콤비네이터 — `:Apply`/`:Compute`/`Tween{...}` 조합, base 프리미티브 아님(`research/tween-plan.md`) + ├── Animate.luau # `Animate(info)` 편의 콤비네이터 — `factory(self)->State`, `:Apply`로 붙임(내부는 `:Compute`/`Tween{...}` 조합), base 프리미티브 아님(`research/tween-plan.md`) ├── DI/ │ └── init.luau # 제네릭 생성자 + ~25개 정적 필드(UIInstances) └── init.luau diff --git a/.claude/base/bind-system-plan.md b/.claude/base/bind-system-plan.md index dc3fd31..3e37427 100644 --- a/.claude/base/bind-system-plan.md +++ b/.claude/base/bind-system-plan.md @@ -1602,6 +1602,18 @@ State/Source도 `:With`/`:Compute`마다 새 노드가 나오는 같은 모양 별개 기능** — 커링은 "`fn` 자체를 팩토리로 짜는 관용구" 권장이고, `:Apply`는 그렇게 만든 팩토리를 체이닝 문법으로 적용하는 수단. 둘이 합쳐지면 `state:Apply(makeFormatter("ko-KR"))`처럼 자연스럽게 이어짐. +- **관용구 — 이름 붙여 재사용하는 콤비네이터는 항상 `:Apply`로 붙인다 + (2026-08-12 세션, `research/operator-sugar-plan.md`/`research/ + tween-plan.md`의 `Animate` 정정에서 도출)**: 그 자리에서 한 번 쓰고 + 마는 인라인 람다(deps도 그 호출문에 바로 나열)는 `:Compute(fn, + ...deps)`를 직접 쓰고, `local addTax = Sum(tax, shipping)`처럼 이름 + 붙여 여러 곳에서 재사용할 콤비네이터는 인자 개수(0항/N항)와 무관하게 + 전부 `factory(self) -> State`를 반환해 `:Apply`로 붙임 — 스타일 + 선호가 아니라 정합성 문제: quad는 암묵적 자동 추적을 기각했으므로 + (위 "암묵적 자동 추적 기각" 절) 재사용 팩토리가 캡처한 deps를 + `:Compute`에 직접 꽂으면 그 deps가 구독 목록에 안 걸려 조용히 + 멈추는 버그가 됨 — `:Apply`는 factory 내부에서 `self:Compute(fn, + ...deps)`를 스스로 다시 전달하므로 이 문제가 없음. **Observer/Effect의 `:Subscribe()`/`:Unsubscribe()`는 이 절과 무관한 별개 주제** — 아래 새 절로 분리(이전에 이 헤더 아래 잘못 걸려 있던 diff --git a/.claude/question.md b/.claude/question.md index 3034709..d8828a1 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -208,6 +208,12 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만** ### 3. 낮은 우선순위 +- **`Operator` 콤비네이터 슈가 네임스페이스 이름(2026-08-12 신설)** — + `Sum`/`Product`/`Not`/비트연산 등 `:Compute`/`:Apply`용 슈가 함수 모음의 + 이름. 흔한 단어라 top-level 노출은 위험, 후보는 `Operator`/`Op`/`Ops` + (`Combinator`는 코퍼스 전반에서 이미 일반명사로 쓰여서 제외) — 아직 + 미정. `research/operator-sugar-plan.md` 참고. 구현 자체는 맨 마지막 + 우선순위(순수 슈가, 없어도 무방). - `research/existing-instance-bind-plan.md` — 스코프 논의만 필요, 구현 착수를 막지 않음. - **v1 `objectListClass.__newIndex` 오타 기능의 재현 테스트 필요** — diff --git a/.claude/research/operator-sugar-plan.md b/.claude/research/operator-sugar-plan.md new file mode 100644 index 0000000..7d41e3f --- /dev/null +++ b/.claude/research/operator-sugar-plan.md @@ -0,0 +1,195 @@ +# Operator 콤비네이터 슈가 — Sum/Product/Not/비트연산 등 + +**상태**: research — 사용자 요청(2026-08-12 세션)으로 신설, 같은 세션 +후속 논의에서 `:Apply` 경유로 확정(아래 "왜 `:Apply`인가" 절). **구현은 +맨 마지막으로 미룸(사용자 본인이 명시)**: 순수 슈가라 없어도 quad 기능상 +문제없고, 각 함수가 서로 의존이 없어 나중에 통째로 추가하거나 개별 +함수를 지우고 고쳐도 안전 — 우선순위가 낮은 이유. **지금 이 문서를 쓰는 +목적은 구현 착수가 아니라 설계/네이밍 논의를 미리 남겨두는 것뿐.** + +## 동기 + +`:Compute`/`:Apply`는 `fn(self, ...)` 람다를 요구하는데, `not self:Get()`처럼 +정말 단순한 연산에도 매번 `function(self) return not self:Get() end`를 +쓰는 건 번거롭고, 같은 패턴이 코드 여기저기 반복되면 가독성도 떨어짐. +기본 연산(산술/논리/비트)을 미리 만들어둔 콤비네이터로 표현하면 보기도 +간결해지고 유지보수도 쉬워짐 — 예: + +```lua +-- 지금(람다 매번 작성) +reduceMotion:Compute(function(self) return not self:Get() end) + +-- 슈가로 +reduceMotion:Apply(Operator.Not) -- 네임스페이스 이름은 미정, 아래 참고 +``` + +## 메커니즘 — 새 프리미티브 아님, `:Apply(factory)` 위의 순수 함수 + +**모든 `Operator.*`는 항상 `factory(self) -> State` 모양이고 항상 +`:Apply`로 붙인다** — 인자 없는 것(`Not`)과 커링되는 것(`Sum(a,b,c)`)을 +구분하지 않고 하나의 규칙으로 통일(아래 "왜 `:Apply`인가" 절 근거). +내부적으로는 `self:Compute(...)`를 호출해 실제 반응형 노드를 만드는 +것뿐 — 새 State/Handler 카테고리 불필요. + +```lua +-- 0항(자기 자신만 변환) — 그 자체로 이미 factory(self) 모양 +Operator.Not = function(self) + return self:Compute(function(h) + return not h:Get() + end) +end + +-- 사용 +reduceMotion:Apply(Operator.Not) +``` + +```lua +-- N항(self + 다른 state들을 결합) — 커링: Sum(a,b,...)가 factory를 반환 +function Operator.Sum(...: State) + local deps = {...} + return function(self: State) + return self:Compute(function(selfH, previous, ...) + local total = selfH:Get() + for _, h in {...} do + total += h:Get() + end + return total + end, table.unpack(deps)) + end +end + +-- 사용 — 한 번 만들어 이름 붙여 재사용 가능 +local addTaxAndShipping = Operator.Sum(tax, shipping) +price:Apply(addTaxAndShipping) +``` + +`Product`/`And`/`Or`/`Xor`/`Band`/`Bor`/`Bxor`/`Bnot`/`Shl`/`Shr` 등도 전부 +같은 두 형태(0항은 그 자체가 `factory(self)`, N항은 커링해서 `factory(self)`를 +반환) — 어느 쪽이든 항상 `:Apply`로 붙임. 비트 연산은 Luau에 연산자가 +없어 `bit32` 라이브러리 위에 얇게 얹는 형태가 됨. + +## 왜 `:Apply`인가 — 스타일이 아니라 정합성 문제 (2026-08-12 세션, 후속 논의) + +**처음엔 "0항은 `:Compute`에 바로, N항만 `:Apply`"로 나눠 썼으나 사용자가 +일관성 문제로 재검토를 요청, 논의 중 실제 정합성 문제까지 발견되어 +`:Apply` 통일로 확정.** + +1. **재사용 가능한 커링 팩토리는 `:Compute`로는 안전하게 못 만든다 — + 진짜 버그 가능성.** quad는 Vide식 암묵적 자동 추적을 이미 기각했음 + (`bind-system-plan.md` "암묵적 자동 추적 기각") — 의존성은 오직 + `:With`/`:Compute`의 **그 호출문 자체**에 나열된 trailing args로만 + 등록됨. 그래서 `local addTax = Sum(tax, shipping)`처럼 한 번 만들어 + 재사용하고 싶은 값을 `price:Compute(addTax)`처럼 바로 꽂으면, + `addTax` 내부에서 클로저로 캡처한 `tax`/`shipping`을 아무리 `:Get()` + 해도 **`:Compute`의 구독 목록엔 안 걸림** — `tax`/`shipping`이 + 바뀌어도 조용히 재계산이 안 일어나는 버그가 됨. 이걸 피하려면 + `price:Compute(addTax, tax, shipping)`처럼 이미 `Sum(...)`에 넘긴 + deps를 호출부에서 또 나열해야 하는데, 이게 바로 2026-08-11 세션에서 + "trailing deps를 fn 위치 인자로 노출"하게 만든 그 중복/드리프트 + 위험(`bind-system-plan.md` 해당 절)과 완전히 같은 클래스의 문제 — + 재사용 가능한 이름을 만드는 의미 자체가 없어짐. + **`:Apply`는 이 문제가 원천적으로 없음**: factory가 내부에서 + `self:Compute(fn, tax, shipping)`을 직접 호출해 자기가 캡처한 deps를 + 스스로 다시 넘기므로(호출자가 재입력하는 게 아니라 factory 자신이 + 한 번 캡처한 값을 그대로 전달), 중복 없이 안전하게 재사용됨 — + `price:Apply(addTax)`, `otherPrice:Apply(addTax)` 둘 다 안전. +2. **기존 문서 관용구와 일치.** `bind-system-plan.md`의 `:Apply` 절이 + 이미 `state:Apply(makeFormatter("ko-KR"))`를 "커링 팩토리 + `:Apply`"의 + 정석 예시로 들어둠 — `Operator.*`/`Animate`가 이 관용구를 따르는 게 + 자연스러움. `Animate`가 `:Compute`를 골랐던 건 오히려 이 기존 + 관용구에서 벗어난 예외였다는 게 이번 논의에서 드러남(`research/ + tween-plan.md` "왜 `:Apply`인가로 정정" 절 참고). +3. **일관성 — 0항/N항을 나누지 않음.** `Not`은 deps가 없어서 위 1번 + 문제와 무관하지만, "이 라이브러리의 콤비네이터는 항상 `:Apply`로 + 붙인다"는 단일 규칙을 지키는 게 "0항만 예외적으로 `:Compute`에 + 바로 꽂는다"는 케이스 분기를 사용자가 매번 기억해야 하는 것보다 + 낫다는 게 사용자 판단. 비용은 `Not`이 내부적으로 `self:Compute(...)` + 한 겹을 더 감싸는 것뿐 — 무시할 만한 오버헤드. +4. **의미론도 더 맞음.** "값에 연산자를 적용한다"는 게 "값으로부터 + 완전히 새로운 파생값을 계산한다"보다 더 정확한 표현 — `:Compute`가 + v1/Fusion류 "매 스텝 능동적으로 값을 갱신"하는 것처럼 읽힐 수 있다는 + 우려도 사용자가 제기(오해일 뿐 실제 동작은 pull-recompute지만, 읽는 + 사람에게 주는 인상까지 고려). + +**`:Compute`의 역할 재확인**: 이걸로 `:Compute`가 필요 없어지는 게 +아니라, 역할이 명확해짐 — `:Compute(fn, ...deps)`는 **그 자리에서 한 번 +쓰고 마는 인라인 람다**(deps도 그 호출문에 바로 나열) 전용 저수준 +프리미티브로 남고, **이름 붙여 재사용하는 콤비네이터**(라이브러리가 +제공하는 것이든 사용자가 직접 만드는 것이든)는 전부 `factory(self)` 모양 ++ `:Apply`로 통일. `Operator.*`/`Animate`의 내부 구현은 여전히 +`:Compute`를 쓴다 — 사용자에게 노출되는 표면만 `:Apply`. + +## 미래 고려사항 (보류) — 중첩 결합 `Sum(a, b, Sum(c, d))` flatten 최적화 + +**사용자 제기(2026-08-12 세션), 지금은 착수 안 함 — 실사용 사례가 나오면 +재검토.** `Sum(a, b, Sum(c, d))`처럼 콤비네이터를 중첩하는 것 자체는 +**지금 설계로도 이미 가능** — `Sum(c, d)`를 먼저 실제 `self`에 적용해 +구체적인 `State`로 만든 뒤(`c:Apply(Operator.Sum(d))`), 그 결과를 +바깥 `Sum`의 평범한 operand로 넘기면 됨. 다만 이러면 안쪽 `Sum(c,d)`가 +독립된 State 노드를 하나 더 만들어서(중첩 `:Compute`), 바깥 `Sum`이 +`a+b+c+d`를 한 번에 계산하는 것보다 그래프 레이어가 한 겹 더 생김. + +사용자가 제안한 최적화 방향: `Sum(...)`이 리턴하는 클로저가 자기가 +캡처한 operand 목록(`local keep = {c, d}`)을 **약한 릴레이션**(`Relate`, +`base/relate-plan.md`)으로 그 클로저 자신에 붙여두면, 나중에 다른 `Sum` +호출이 자신의 operand 중 하나가 "이미 Operator 콤비네이터가 만든 +클로저"임을 감지해서 그 안에 보관된 operand들을 꺼내 자기 자신의 operand +목록에 합쳐 넣을 수 있음(`Sum(a, b, Sum(c, d))` → 실질적으로 `Sum(a, b, c, +d)`와 동일한 단일 `:Compute` 노드로 flatten) — 클로저가 GC되면 약한 +릴레이션도 같이 사라지므로 메모리 누수 없음. + +**보류 이유**: 순수 최적화(그래프 노드 한 겹 줄이기)일 뿐 기능 격차가 +아님 — 지금도 위 방법으로 중첩 자체는 문제없이 됨. `Sum`류 생성 팩토리에 +introspection 로직을 추가하는 거라 라이브러리 복잡도가 늘어남 — 사용자 +본인이 "실제 사용사례를 보고 필요한지 나중에 검토"로 명시적으로 유보. +나중에 착수하게 되면 `Sum`/`Product` 등 각 생성 팩토리를 개별적으로 살짝 +고치면 되는 수준이라, 지금 다른 설계에 영향 주지 않음. + +## 패키지 배치 + +`quad-base` — Store/State 계층 위에서만 동작하는 순수 함수라 엔진 종속 +없음(`quad-roblox` 아님). `Animate`가 `Tween`과 함께 어디에 배치됐는지와 +같은 결로 맞추면 됨(`research/tween-plan.md` 참고, 단 `Animate` 자체는 +`Tween`이 quad-roblox 개념(`PropertyHandler`)에 연결되므로 quad-roblox +배치 — Operator 슈가는 그런 엔진 종속이 없다는 점이 다름). + +## 열린 질문 — 네임스페이스 이름 (미정) + +`Not`/`Sum`/`And`/`Or` 같은 이름은 흔한 단어라 top-level에 그냥 두면 +충돌 위험이 큼 — `Tag`/`Attribute`처럼 네임스페이스로 묶여야 함 +(`Operator.Not`처럼). 문제는 **짧으면서 "이 연산자 콤비네이터 슈가 +모음"이라는 목적을 잘 담는 이름을 아직 못 찾음** — 사용자가 직접 이 +문제를 제기(2026-08-12 세션). 코퍼스 전체에 `Operator`/`Op` 이름 충돌은 +없음을 확인함(grep 결과 없음), 아래는 후보: + +- **`Operator`** — 의미는 제일 정확(산술/논리/비트 전부 "연산자"로 + 포괄). 다만 다소 길어서 `Operator.Sum(a, b)`처럼 매번 타이핑하기엔 + 무거울 수 있음. +- **`Op`** — 짧지만 무엇의 축약인지 처음 보면 바로 안 와닿을 수 있음. +- **`Ops`** — `Op`의 복수형, 뉘앙스는 비슷. +- ~~`Combinator`~~ — 코퍼스 전반에서 `:Apply`/`Animate` 같은 패턴을 + 설명할 때 이미 일반명사로 "콤비네이터"라는 말을 자주 써서(예: + `modifier-plan.md` 8번 절), 네임스페이스 이름으로 쓰면 "이 특정 + 모듈"과 "패턴을 가리키는 일반 용어"가 헷갈릴 수 있어 후보에서 제외. + +`.claude/question.md` 3번(낮은 우선순위)에도 반영. 용어 정리 라운드 +(`question.md` 1번, `Brand`/`Tag`류)와 같은 카테고리로 나중에 같이 +검토해도 됨 — 급하지 않음. + +## 열린 질문 — 포함 범위 + +- 산술(`Sum`/`Product`/`Sub`/`Div`?)·논리(`Not`/`And`/`Or`/`Xor`)·비트 + (`Band`/`Bor`/`Bxor`/`Bnot`/`Shl`/`Shr`)까지는 비교적 명확한데, 비교 + 연산자(`Eq`/`Lt`/`Gt`/`Lte`/`Gte`)까지 포함할지는 미정 — 포함해도 같은 + 패턴(커링 팩토리 + `:Apply`)으로 자연스럽게 들어감. +- `Sum(a, b, ...)`가 self까지 포함해서 더하는 형태(위 예시)로 확정 — + 사용자 원 예시(`:Apply(Sum(state, state...))`)와 일치. self 없이 여러 + state를 독립적으로 합치는 형태가 따로 필요한지는 실사용 사례가 나오면 + 재검토(지금은 `Store.Combine`류로 이미 커버된다고 봄). + +## 우선순위 + +**맨 마지막.** 없어도 quad는 기능상 완전하고, 함수 간 의존이 없어 나중에 +일부만 먼저 만들거나 전부 미뤄도 리스크가 없음. 지금은 이 문서로 +동기/모양/열린 질문만 남겨두고, 실제 구현은 다른 마일스톤이 다 끝난 뒤로 +미룸. diff --git a/.claude/research/tween-plan.md b/.claude/research/tween-plan.md index c14c2bf..642f466 100644 --- a/.claude/research/tween-plan.md +++ b/.claude/research/tween-plan.md @@ -12,7 +12,10 @@ tweenData...)] = storeValue`)은 `archive/tween-special-bind-key-reversed.md`로 **2026-08-12 세션에서 옵션 값 모양+override 정책 이름+`Animate` 콤비네이터 시그니처까지 전부 확정됨**(아래 "확정: `Tween{...}` 최종 모양"/"`Animate` -콤비네이터" 절) — 남은 건 자연 완료(Completed) 시 북키핑 정리 여부뿐. +콤비네이터" 절), **같은 날 후속 논의에서 `Animate`의 호출 경로가 +`:Compute` 직결 → `:Apply`로 정정됨**(아래 "왜 `:Apply`로 정정됐는가" 절, +`research/operator-sugar-plan.md`와 같은 근거) — 남은 건 자연 완료 +(Completed) 시 북키핑 정리 여부뿐. `initValue`는 사용자가 필요해지면 직접 처리하기로 확정(에이전트 작업 범위에서 제외, 아래 해당 절 참고). 원본: `.claude/initreq/raw-userinput.md` "트윈은 어떻게 할 것이냐" / "스토어 값은 @@ -204,29 +207,34 @@ local function resolve(v) end end +-- Animate(info)는 factory(self) -> State를 반환 — :Apply 전용 +-- (2026-08-12 세션 후속 논의로 :Compute 직결에서 정정됨, 아래 +-- "왜 `:Apply`로 정정됐는가" 절 참고) local function Animate(info) return function(self) - local v = self:Get() + return self:Compute(function(selfH) + local v = selfH:Get() - local canAnimate = resolve(info.CanAnimate) - if canAnimate == nil then - canAnimate = true -- CanAnimate 생략 시 기본 애니메이션 활성 - end - if not canAnimate then - return v -- Tween로 안 감쌈 — 그대로 plain 값 반환(애니메이션 우회) - end + local canAnimate = resolve(info.CanAnimate) + if canAnimate == nil then + canAnimate = true -- CanAnimate 생략 시 기본 애니메이션 활성 + end + if not canAnimate then + return v -- Tween로 안 감쌈 — 그대로 plain 값 반환(애니메이션 우회) + end - return Tween{ - Value = v, - Info = resolve(info.Info), - Time = resolve(info.Time), - Style = resolve(info.Style), - Direction = resolve(info.Direction), - RepeatCount = resolve(info.RepeatCount), - Reverses = resolve(info.Reverses), - DelayTime = resolve(info.DelayTime), - Override = resolve(info.Override), - } + return Tween{ + Value = v, + Info = resolve(info.Info), + Time = resolve(info.Time), + Style = resolve(info.Style), + Direction = resolve(info.Direction), + RepeatCount = resolve(info.RepeatCount), + Reverses = resolve(info.Reverses), + DelayTime = resolve(info.DelayTime), + Override = resolve(info.Override), + } + end) end end ``` @@ -244,7 +252,7 @@ reduceMotion류 접근성 우회가 이 필드 하나로 바로 표현됨: ```lua -- reduceMotion: State -Position = mySource:Compute(Animate{ +Position = mySource:Apply(Animate{ Style = Enum.EasingStyle.Bounce, CanAnimate = reduceMotion:Compute(function(r) return not r:Get() end), }) @@ -261,29 +269,51 @@ State여도..." 절과 같은 이유. `CanAnimate`로 통일 — 이 필드 하나만 다른 케이싱을 쓸 특별한 이유가 없다고 판단(확정은 아님, 다음 세션에 뒤집혀도 비용 낮음). -**왜 `:Compute`에 직접 넘길 수 있는가**: `Animate(info)`가 반환하는 -`function(self) ... end`는 `:Compute(fn)`의 콜백 시그니처(`fn(self, -previous?, ...deps)`, `self`는 raw 값이 아니라 lazy State 핸들 — -`bind-system-plan.md` "self/with 값 둘 다 lazy State 핸들로 통일" 절)와 -정확히 일치 — 그래서 `state:Compute(Animate{Style=...})`처럼 **바로** -넘기면 됨, 예전 `useTween` 스케치가 필요로 했던 `:Apply` 경유가 불필요: +**왜 `:Apply`로 정정됐는가(2026-08-12 세션 후속 논의)**: 처음엔 `Animate(info)`가 +`:Compute(fn)`의 콜백 시그니처(`fn(self, previous?, ...deps)`)와 모양이 +정확히 일치한다는 이유로 `state:Compute(Animate{...})`처럼 바로 넘기고 +`:Apply` 경유를 "불필요한 한 겹"으로 보고 피했음. 이후 `research/ +operator-sugar-plan.md`의 비슷한 콤비네이터(`Sum`/`Not` 등) 논의에서 +재검토됨 — 결론은 반대: **재사용 가능한 이름 붙은 콤비네이터는 스타일이 +아니라 정합성 때문에 `:Apply`가 맞음.** + +- `Animate(info)` 자체는 옵션이 deps로 등록되지 않아(아래 절) 이 특정 + 사례에서 `:Compute` 직결이 실제로 깨지진 않았지만, 같은 패밀리인 + `Sum(a,b,c)`류는 `local addTax = Sum(tax, shipping)`처럼 만들어서 + 재사용하려는 순간 `:Compute` 직결이 실제로 깨짐(quad는 Vide식 암묵적 + 자동 추적을 이미 기각해서, `tax`/`shipping`을 클로저로만 읽으면 그 + 값이 바뀌어도 재계산이 안 트리거됨 — `:Compute`의 구독 목록은 오직 + 그 호출문 자체의 trailing args로만 채워짐). `:Apply`는 factory가 + 내부에서 `self:Compute(fn, tax, shipping)`을 스스로 다시 전달하므로 + 이 문제가 없음. 상세 근거는 `research/operator-sugar-plan.md` "왜 + `:Apply`인가" 절 참고. +- **일관성**: "이 라이브러리가 제공하는 이름 붙은 콤비네이터는 항상 + `:Apply`로 붙인다"는 단일 규칙이, "`Animate`만 예외적으로 `:Compute` + 직결"보다 기억하기 쉬움 — `bind-system-plan.md`가 이미 `:Apply` 절에서 + `state:Apply(makeFormatter("ko-KR"))`를 커링 팩토리의 정석 예시로 + 들어둔 것과도 맞음(오히려 원래 `Animate`의 `:Compute` 선택이 이 + 기존 관용구에서 벗어난 예외였음). + +**결론 — `Animate(info)`는 `factory(self) -> State`를 반환하고 항상 +`:Apply`로 붙인다**(위 구현 코드 블록도 이렇게 갱신됨 — 내부에서 +`self:Compute(...)`를 직접 호출): ```lua -- 슈거로 충분한 흔한 케이스 -Position = mySource:Compute(Animate{Style = Enum.EasingStyle.Bounce, Time = 0.3}) +Position = mySource:Apply(Animate{Style = Enum.EasingStyle.Bounce, Time = 0.3}) ``` **`Style`/`Override` 등이 State여도 값 변경 자체가 재애니메이션을 -트리거하지 않음 — 의도된 동작.** `Animate{...}`가 반환한 `fn`은 -`self`(= `mySource`, `Value`가 될 State)가 바뀔 때만 `:Compute`에 -의해 다시 불림 — `info.Style`이 State여도 `:Compute`의 구독 목록에 -안 걸림(`resolve`가 그냥 `fn` 본문 안에서 클로저로 읽을 뿐, `:With`/ -trailing-deps로 선언 안 됨). 그래서 `Style`이 바뀌어도 그 자체로는 -아무 일도 안 일어나고, 다음에 `Value`가 실제로 바뀔 때 그 시점의 -최신 `Style`이 자연히 반영됨. 사용자가 직접 짚은 근거: "style 같은 -게 바뀐다고 다시 애니메이션을 수행하는 경우는 없다" — 실사용 요구와 -정확히 일치하는 동작이라 별도 트리거 배선이 오히려 불필요한 -복잡도였을 것. +트리거하지 않음 — 의도된 동작(이 부분은 `:Apply`로 바뀌어도 동일).** +`Animate{...}`가 반환한 factory 내부의 `self:Compute(fn)` 호출은 +`selfH`(= `mySource`, `Value`가 될 State)가 바뀔 때만 다시 불림 — +`info.Style`이 State여도 이 내부 `:Compute`의 trailing deps로 안 +넘어가므로 구독 목록에 안 걸림(`resolve`가 그냥 `fn` 본문 안에서 +클로저로 읽을 뿐). 그래서 `Style`이 바뀌어도 그 자체로는 아무 일도 안 +일어나고, 다음에 `Value`가 실제로 바뀔 때 그 시점의 최신 `Style`이 +자연히 반영됨. 사용자가 직접 짚은 근거: "style 같은 게 바뀐다고 다시 +애니메이션을 수행하는 경우는 없다" — 실사용 요구와 정확히 일치하는 +동작이라 별도 트리거 배선이 오히려 불필요한 복잡도였을 것. **구 `useTween`(reduceMotion 우회) 스케치는 `CanAnimate` 필드로 대체됨** (위 "`CanAnimate`" 절) — 흔한 단순 토글은 그걸로 충분. 이전에 검토했던 @@ -294,14 +324,18 @@ trailing-deps로 선언 안 됨). 그래서 `Style`이 바뀌어도 그 자체 불필요: ```lua -Position = mySource:Compute(function(self) +Position = mySource:Apply(function(self) if someComplexCondition() then - return someOtherValue + return self:Compute(function(h) return someOtherValue end) end return Animate{Style = Enum.EasingStyle.Bounce}(self) end) ``` +(`Animate{...}(self)`가 이제 plain 값이 아니라 `State`를 반환하므로, +탈출 분기도 똑같이 `self:Compute(...)`로 감싸 타입을 맞춰야 함 — +`:Apply`로 붙이는 factory는 항상 `State`를 반환해야 한다는 불변식.) + **base 프리미티브 아님 — 여전히 quad-roblox 유틸**(아래 "패키지 경계" 절) — `Tween` 값 타입/`isTween`만 base(`quad-base/Tween.luau`)에 있고, `Animate`는 이미 있는 `:Compute`/`Tween{...}`/`isState`를 조합한 diff --git a/.claude/session/2026-08-12-05-operator-sugar-plan.md b/.claude/session/2026-08-12-05-operator-sugar-plan.md new file mode 100644 index 0000000..8baef3b --- /dev/null +++ b/.claude/session/2026-08-12-05-operator-sugar-plan.md @@ -0,0 +1,89 @@ +# 2026-08-12 다섯 번째 세션 — Operator 콤비네이터 슈가 신설 + +## 배경 + +사용자가 `reduceMotion:Compute(function(r) return not r:Get() end)`류 +람다가 정말 단순한 연산(not, +, ...)에도 매번 필요한 게 번거롭다고 +지적 — 기본 연산(산술/논리/비트)을 미리 콤비네이터로 만들어두면 +`:Compute(Not)`, `:Apply(Sum(a, b))`처럼 짧게 쓸 수 있고, 결합해서 +표현하면 가독성/유지보수도 좋아진다는 제안. + +## 논의 + +메커니즘 자체는 새로 필요한 게 없다는 걸 바로 확인함 — 2026-08-12 +두 번째 세션에서 확정된 `Animate(info)` 패턴(`function(self)...end`을 +반환해 `:Compute`/`:Apply`의 self-lazy-핸들 계약에 바로 꽂히는 것)과 +정확히 같은 모양: + +- 단항(`Not`)은 그냥 `fn(self)` — `:Compute(Not)`로 바로 씀. +- 다항(`Sum(a, b, ...)`)은 `factory(self)`를 반환해 `:Apply(Sum(a,b))`로 + self까지 포함해서 결합. + +각 함수가 서로 독립(공유 상태/의존 없음)이라 부분적으로 나중에 +추가하거나 하나만 고쳐도 다른 함수에 영향이 없다는 것도 확인 — 이게 +우선순위를 맨 마지막으로 미뤄도 되는 근거. 사용자도 "이건 문서화 준비는 +미리 적어두는 게 맞지만 구현 자체는 정말 맨 마지막에 해도 된다"고 직접 +우선순위를 명시. + +막힌 지점은 네이밍뿐이었음 — `Not`/`Sum`/`And`처럼 흔한 이름을 top-level에 +그냥 두면 충돌 위험이 커서 `Tag`/`Attribute`처럼 네임스페이스가 필요한데, +사용자가 예시로 든 `Operator.Not`이 마음에 드는지 스스로도 확신이 없다고 +언급. 코퍼스 전체에 `Operator`/`Op` 이름 충돌이 없는지 grep으로 확인(충돌 +없음), `Combinator`는 코퍼스 전반에서 `:Apply` 패턴을 설명할 때 이미 +일반명사로 자주 쓰여서("Apply 콤비네이터") 네임스페이스 이름으로 쓰면 +헷갈릴 수 있어 후보에서 제외 — 최종 이름은 `Operator`/`Op`/`Ops` 중 +미정으로 남김. + +## 결과 (1차) + +`research/operator-sugar-plan.md` 신설(동기/메커니즘/패키지 배치(quad-base, +엔진 종속 없음)/열린 질문 두 가지(네임스페이스 이름, 포함 범위·`Sum`이 +self를 포함하는 형태가 맞는지) 전부 기록). `README.md` research 표, +`question.md` 3번(낮은 우선순위)에 반영. 구현은 착수 안 함 — 사용자가 +직접 맨 마지막 우선순위로 지정했으므로 이 세션은 설계/문서화만. + +이때 초안은 "0항(`Not`)은 `:Compute`에 바로, N항(`Sum`)만 `:Apply`용 +팩토리"로 나눠서 썼음 — 아래 후속 논의에서 뒤집힘. + +## 후속 논의 — `:Compute` vs `:Apply`, 어느 쪽이 맞는가 + +사용자가 바로 재검토를 요청: 가독성상 `:Apply(Sum(a,b,c))`가 나아 +보이고, `local SumFn = Sum(...)`처럼 만들어 여러 곳에서 `:Apply(SumFn)`로 +재사용하는 게 커링의 실제 이점인데 `:Compute`만 쓰면 그게 무색해진다는 +지적. `Animate`도 같은 이유로 `:Compute`보다 `:Apply`가 맞지 않냐는 +질문도 같이 나옴 — "값에 Tween을 적용한다"는 맥락이지 "계산한다"는 +맥락이 아니라는 의미론적 근거, `:Compute`가 v1/Fusion류 "매 스텝 능동 +갱신"처럼 읽힐 수 있다는 우려도 제기. + +검토 결과 **스타일이 아니라 진짜 정합성 문제였음**을 확인: quad는 Vide식 +암묵적 자동 추적을 이미 기각했으므로(`bind-system-plan.md` "암묵적 자동 +추적 기각"), `local addTax = Sum(tax, shipping)`처럼 만든 값을 +`price:Compute(addTax)`에 바로 꽂으면 `addTax`가 클로저로 캡처한 +`tax`/`shipping`이 `:Compute`의 구독 목록(그 호출문의 trailing args만 +등록됨)에 안 걸려 조용히 재계산이 멈추는 버그가 됨 — 이게 바로 +2026-08-11 세션이 "trailing deps를 fn 위치 인자로 노출"하게 만든 것과 +같은 클래스의 중복/드리프트 문제. `:Apply`는 factory가 내부에서 +`self:Compute(fn, tax, shipping)`을 스스로 다시 전달해 이 문제가 +원천적으로 없음 — 재사용 가능한 이름 붙은 콤비네이터는 `:Apply`가 +유일하게 안전한 경로. 기존 문서(`bind-system-plan.md`의 `:Apply` 절이 +이미 `state:Apply(makeFormatter("ko-KR"))`를 정석 예시로 들어둔 것)와도 +맞아떨어짐 — 오히려 `Animate`가 애초에 `:Compute`를 골랐던 게 이 관용구의 +예외였다는 게 드러남. + +## 결과 (최종) + +- `research/operator-sugar-plan.md`: "0항은 Compute, N항은 Apply" 분기를 + 버리고 **전부 `factory(self) -> State` + `:Apply`로 통일**("왜 `:Apply`인가" + 절 신설, 정합성 근거 전문 기록). +- `research/tween-plan.md`: `Animate(info)`를 `function(self) return + self:Compute(...) end`로 바꿔 `:Apply` 전용으로 정정("왜 `:Apply`로 + 정정됐는가" 절 신설), 모든 사용 예시(`:Compute(Animate{...})` → + `:Apply(Animate{...})`)와 커스텀 조건 이스케이프 예시까지 같이 수정. + `Animate` 자체의 시그니처/동작(옵션이 deps로 안 걸리는 것 등)은 그대로 — + 바뀐 건 호출 경로뿐. +- `base/bind-system-plan.md`의 `:Apply` 절에 "이름 붙여 재사용하는 + 콤비네이터는 항상 `:Apply`" 관용구를 일반 원칙으로 추가 — 나중에 + 비슷한 콤비네이터를 또 만들 때 케이스마다 재논의 안 해도 되게. + +구현은 여전히 착수 안 함(맨 마지막 우선순위 유지) — 이번 논의는 순전히 +설계/문서 정정. diff --git a/CLAUDE.md b/CLAUDE.md index c449ca7..01147b3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -133,10 +133,13 @@ modifier/Ref의 컴포넌트 경계 통과 방식) 논의도 2026-08-04 세션 지원, M0 mock 테스트 하네스와는 별개), 런타임 디버깅 플러그인 `quad-debug`(Studio 플러그인, 실물 Instance→코드 위치 역추적 — 채널 실현 가능성은 실측 검증 완료, 세부 API 이름만 남음), 문서 사이트 전체 - 구조(초심자/api/심화/`quadnomicon` 4축 + 콘텐츠 맵) — 전부 "quad 개발 - 상당 부분 끝난 뒤"로 사용자가 못박은 후순위. 상세는 `.claude/README.md`의 + 구조(초심자/api/심화/`quadnomicon` 4축 + 콘텐츠 맵), `Operator` 콤비네이터 + 슈가(`Sum`/`Product`/`Not`/비트연산 등 `:Compute`/`:Apply`용 — 메커니즘은 + 확정, 네임스페이스 이름만 미정, 구현은 순수 슈가라 맨 마지막) — 전부 "quad + 개발 상당 부분 끝난 뒤"로 사용자가 못박은 후순위. 상세는 `.claude/README.md`의 `research/` 표(`debug-tooling-plan.md`/`documentation-plan.md`/ - `documentation-content-map.md`/`framework-comparison-findings.md`). + `documentation-content-map.md`/`framework-comparison-findings.md`/ + `operator-sugar-plan.md`). 5. 자율 작업 루프/스케줄 설정 여부는 사용자 결정 대기 중 (`HUMAN_TODO.md` 2번 항목). @@ -467,3 +470,22 @@ State 핸들이라는 기존 확정 계약을 놓친 버그. 같은 클래스의 `.claude/` 전역에서 찾아 `base/slot-plan.md` 2곳(`LayoutOrder` 예시, `Slot:Single`)과 `base/tag-plan.md` 1곳에서 추가로 발견·수정. `bind-system-plan.md`에 "이 실수가 반복되기 쉬움" 주의 노트 추가. + +**2026-08-12 다섯 번째 세션 — `Operator` 콤비네이터 슈가 신설, `Animate` +호출 경로를 `:Compute`→`:Apply`로 정정** (`session/2026-08-12-05-operator-sugar-plan.md`) +기본 연산(산술/논리/비트)을 콤비네이터로 쓰는 슈가 제안(`Not`/`Sum` 등, +새 프리미티브 아님). 처음엔 0항은 `:Compute`, N항은 `:Apply`로 나눴으나 +후속 논의로 **재사용 가능한 이름 붙은 콤비네이터는 전부 `:Apply`가 맞다는 +쪽으로 정정** — quad가 암묵적 자동 추적을 기각했기 때문에(`base/ +bind-system-plan.md`) `local addTax = Sum(a,b)`처럼 만든 값을 `:Compute`에 +바로 꽂으면 캡처된 deps가 구독 목록에 안 걸려 조용히 멈추는 진짜 버그가 +됨 — 스타일이 아니라 정합성 문제. 같은 근거로 `research/tween-plan.md`의 +`Animate` 호출 경로도 `:Compute(Animate{...})`→`:Apply(Animate{...})`로 +정정(시그니처/동작 자체는 그대로), `base/bind-system-plan.md`의 `:Apply` +절에 이 관용구를 일반 원칙으로 추가. 네임스페이스 이름(`Operator`/`Op`/`Ops` +중 미정)만 열린 질문으로 남음. 우선순위는 여전히 사용자가 맨 마지막으로 +직접 지정(순수 슈가, 함수 간 의존 없음) — 구현 착수 안 함. 사용자가 +`:Apply` 통일에 동의, `Sum(a,b,Sum(c,d))` 중첩 flatten 최적화(약한 +`Relate`로 클로저의 operand 목록 추적) 아이디어도 나왔으나 실사용 +사례 나오면 재검토로 보류. `base/architecture.md`의 stale `Animate` +2-인자 시그니처 코멘트도 이 김에 수정.