decide(operator): add Operator sugar plan, unify combinators on :Apply

Sketch Sum/Not/Product-style :Compute/:Apply sugar (research/operator-sugar-plan.md,
implementation deferred). Reusable curried combinators (Sum, Animate) must
go through :Apply, not :Compute — quad rejected implicit auto-tracking, so
a factory's closed-over deps only register if the factory re-declares them
via self:Compute(...) internally; plugging a pre-built factory straight
into :Compute silently drops reactivity. Flip Animate's call site
accordingly in tween-plan.md, add the convention to bind-system-plan.md's
:Apply section, and fix a stale two-arg Animate signature comment in
architecture.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-12 11:54:09 +09:00
parent 34ded8b953
commit 9909aca227
Signed by: qwreey
GPG key ID: D28DB79297A214BD
8 changed files with 404 additions and 45 deletions

View file

@ -56,7 +56,7 @@
| 문서 | 내용 | 우선순위 |
|---|---|---|
| `tween-plan.md` | **[2026-08-12 세션에서 구조+옵션 값 모양+override 정책+`Animate` 콤비네이터까지 전부 확정]** 값-레벨 `Tween<T>` 래퍼(PropertyHandler가 소비), 구 특수 bind key 모델은 `archive/tween-special-bind-key-reversed.md`로 이전. 3-상태 릴레이션 슬롯(`{Tween,Value}\|true\|nil`), `T'=T\|Tween<T>` 타입 치환. 옵션 값 모양은 `Info: TweenInfo?` 우선+편의 필드(`Time`/`Style`/...) 폴백, override는 `Tween.Cancel`(기본)/`Tween.Finish` 2값. `Animate(info)``Tween` opts(`Value` 제외)를 `T\|State<T>`로 받아 `:Compute`에 바로 넘기는 sugar로 확정(구 `useTween` 스케치 대체). `initValue`는 사용자가 직접 처리(에이전트 범위 제외), 남은 건 자연완료 북키핑 하나뿐 | 하 — 사실상 다 닫힘 |
| `tween-plan.md` | **[2026-08-12 세션에서 구조+옵션 값 모양+override 정책+`Animate` 콤비네이터까지 전부 확정]** 값-레벨 `Tween<T>` 래퍼(PropertyHandler가 소비), 구 특수 bind key 모델은 `archive/tween-special-bind-key-reversed.md`로 이전. 3-상태 릴레이션 슬롯(`{Tween,Value}\|true\|nil`), `T'=T\|Tween<T>` 타입 치환. 옵션 값 모양은 `Info: TweenInfo?` 우선+편의 필드(`Time`/`Style`/...) 폴백, override는 `Tween.Cancel`(기본)/`Tween.Finish` 2값. `Animate(info)``Tween` opts(`Value` 제외)를 `T\|State<T>`로 받아 `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/` — 완료됐거나 완전히 뒤집힌 것, 능동 참고 불필요

View file

@ -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

View file

@ -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()`는 이 절과 무관한
별개 주제** — 아래 새 절로 분리(이전에 이 헤더 아래 잘못 걸려 있던

View file

@ -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` 오타 기능의 재현 테스트 필요**

View file

@ -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<U>` 모양이고 항상
`: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<number>)
local deps = {...}
return function(self: State<number>)
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<number>`로 만든 뒤(`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는 기능상 완전하고, 함수 간 의존이 없어 나중에
일부만 먼저 만들거나 전부 미뤄도 리스크가 없음. 지금은 이 문서로
동기/모양/열린 질문만 남겨두고, 실제 구현은 다른 마일스톤이 다 끝난 뒤로
미룸.

View file

@ -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<boolean>
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<T>` 값 타입/`isTween`만 base(`quad-base/Tween.luau`)에
있고, `Animate`는 이미 있는 `:Compute`/`Tween{...}`/`isState`를 조합한

View file

@ -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`" 관용구를 일반 원칙으로 추가 — 나중에
비슷한 콤비네이터를 또 만들 때 케이스마다 재논의 안 해도 되게.
구현은 여전히 착수 안 함(맨 마지막 우선순위 유지) — 이번 논의는 순전히
설계/문서 정정.

View file

@ -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-인자 시그니처 코멘트도 이 김에 수정.