decide(bind-system): Compute(fn, ...) trailing-args sugar 확정

Compute 노드는 결과를 담을 새 State 노드를 어차피 만들어야 하므로
추가 의존성 구독을 그 노드에 얹는 건 공짜 sugar로 확정. Effect/Observer는
자기 자신이 State 노드가 아니라 다중 의존성 병합에 새 노드가 실제로
필요해서 동일 sugar를 의도적으로 제외, :With(...)를 코드에 그대로
노출하도록 유지.
This commit is contained in:
qwreey 2026-08-11 10:29:35 +09:00
parent 32601487ae
commit 9370acd394
Signed by: qwreey
GPG key ID: D28DB79297A214BD
5 changed files with 107 additions and 1 deletions

View file

@ -1204,6 +1204,49 @@ lazy State 핸들로 통일, 아래 "Store/State/Source 온톨로지" 절의 "`:
`:Compute`가 원래부터 이 셋 중 제일 먼저 있던 자리라 뒤늦게 문서화된 `:Compute`가 원래부터 이 셋 중 제일 먼저 있던 자리라 뒤늦게 문서화된
것뿐, 새 결정이라기보다 이미 있던 패턴을 명문화한 것. 것뿐, 새 결정이라기보다 이미 있던 패턴을 명문화한 것.
### `:Compute(fn, ...)` — 추가 의존성을 trailing args로 직접 받는 sugar (2026-08-11)
**문제 제기(사용자)**: React의 `useMemo(fn, deps)`처럼 `:With(...)` 없이
`:Compute(fn, a, b, c)`로 바로 추가 의존성을 선언할 수 있으면 더 편하지
않은가 — `self`가 이미 lazy 핸들로 `fn`에 넘어가는 구조라 값 언랩 방식이
아니므로, 예전에 기각된 `Store.Combine({a,b}, function(av,bv)...)`(포지셔널
값 언랩이라 타입 표기가 꼬였던 안)과는 다른 제안.
**확정 — `Compute`엔 채택, `Observer`/`Effect`엔 채택 안 함. 근거는 "새
노드가 실제로 생기는가"의 차이(사용자가 직접 구분).**
- **`:Compute(fn, ...)`는 진짜 공짜 sugar.** `:Compute` 호출은 원래도
결과를 담을 새 State 노드(자기 자신의 계산 캐시 슬롯)를 만들어야
하므로, 그 노드가 `self` 말고 `a,b,c`에도 구독(무효화 엣지)을 추가로
거는 건 **이미 만들어지는 노드에 엣지만 더 얹는 것**`:With(a,b,c):Compute(fn)`
체인(노드 2개: pass-through With 노드 + Compute 노드)과 달리 노드가
안 늘어남(노드 1개). 구현은 `:With(...)`가 이미 하는 "구독 목록 확장"
로직을 Compute 노드 생성 시점에 그대로 적용하는 것뿐 — 새 메커니즘
아님.
- **`Effect(fn, ...)`/`state:Observer(fn, ...)`류 trailing-args 확장은
기각 — 여기선 진짜 새 노드가 생기기 때문.** Effect/Observer는 Compute와
달리 **자기 자신이 결과를 담는 State 노드가 아님**(파생값을 안 만드는
순수 leaf 소비자, `base/store-semantics.md`의 "독립 프리미티브 vs
파생 데이터" 분류에서도 확인되는 차이) — `state`(receiver) 하나만
구독 가능하므로, 의존성이 둘 이상이면 그걸 하나로 합칠 별도 노드가
필요하고 그게 바로 `:With(...)`가 만드는 새 노드임. 이건 절대 공짜가
아니라 **정말 비용이 드는 지점**이라, trailing args로 감춰버리면 "이
줄이 실제로 새 노드/구독을 만든다"는 걸 코드만 보고 알 수 없게 됨 —
`:With`가 clone 빌더가 아니라 진짜 노드로 확정됐던 이유(2026-08-07 세
번째 세션, "코드상의 호출 체인이 그래프 엣지와 1:1로 대응돼야 quad-debug
그래프가 안 꼬임")와 정확히 같은 원칙. 그래서 다중 의존성 Effect/Observer는
**`Effect(fn, state:With(a,b,c))`처럼 `:With` 호출을 코드에 그대로
노출**하도록 유지 — 새 노드가 생기는 지점을 sugar로 숨기지 않는다는
게 핵심.
- **일반 원칙으로 정리**: "trailing args sugar는 그게 정말 무료일 때만
붙인다 — 호출부가 이미 만들어야 하는 노드에 엣지만 얹는 경우(Compute)엔
sugar, 없던 노드를 새로 만들어야 하는 경우(Effect/Observer의 다중
의존성 병합)엔 sugar 없이 `:With`를 명시적으로 남긴다." `quadnomicon`
에세이 후보로 좋음(`research/documentation-content-map.md` 6번 항목
다음에 추가) — "왜 Compute만 여러 deps를 편하게 받고 Effect/Observer는
안 그런가"가 겉보기엔 비일관적으로 보이지만 실제로는 "숨겨지는 비용이
있는가"라는 하나의 원칙에서 나온 것이라는 게 소재.
### `:Compute(fn)`의 선택적 두 번째 인자 — `previous` (무거운 파생 객체 재사용, 2026-08-06) ### `:Compute(fn)`의 선택적 두 번째 인자 — `previous` (무거운 파생 객체 재사용, 2026-08-06)
**배경**: `:Compute`의 결과가 그 자체로 무겁고 재생성 비용이 큰 엔진 **배경**: `:Compute`의 결과가 그 자체로 무겁고 재생성 비용이 큰 엔진

View file

@ -43,7 +43,14 @@ leaf가 죽을 때 **마지막 cleanup을 한 번 더 호출**. 결과적으로
- **다수 의존성은 `:With(...)`로 먼저 하나의 State로 묶어서 넘길 것** - **다수 의존성은 `:With(...)`로 먼저 하나의 State로 묶어서 넘길 것**
React식 별도 deps 배열을 새로 만들지 않음, quad가 이미 가진 다중 의존성 React식 별도 deps 배열을 새로 만들지 않음, quad가 이미 가진 다중 의존성
결합 관용구(`base/bind-system-plan.md` "`:With` + `:Compute`" 절)를 결합 관용구(`base/bind-system-plan.md` "`:With` + `:Compute`" 절)를
그대로 재사용해 같은 일 하는 두 번째 경로를 안 만듦. 그대로 재사용해 같은 일 하는 두 번째 경로를 안 만듦. **`Effect(fn, a, b,
c)`처럼 trailing args로 바로 받는 sugar는 의도적으로 안 만듦**(2026-08-11
세션, `bind-system-plan.md` "`:Compute(fn, ...)` — 추가 의존성을 trailing
args로 직접 받는 sugar" 절 참고) — `Compute`와 달리 Effect/Observer는
자기 자신이 결과를 담는 State 노드가 아니라서, 의존성이 둘 이상이면 그걸
합칠 **새 노드**(`:With`가 만드는 것)가 실제로 필요함. 그 비용을 sugar로
감추지 않고 `Effect(fn, state:With(a,b,c))`처럼 코드에 그대로 드러내는
게 의도된 선택.
- **`fn`은 커링 스타일도 권장(2026-08-07 여섯 번째 세션, 사용자 제안)** — - **`fn`은 커링 스타일도 권장(2026-08-07 여섯 번째 세션, 사용자 제안)** —
`Effect(makeLogger("mount"), state)`처럼 팩토리 함수가 실제 `fn(state)` `Effect(makeLogger("mount"), state)`처럼 팩토리 함수가 실제 `fn(state)`
만들어 반환하는 패턴, `Modifier``Boldify(10)` 커링 관용구(`modifier-plan.md` 만들어 반환하는 패턴, `Modifier``Boldify(10)` 커링 관용구(`modifier-plan.md`

View file

@ -205,6 +205,17 @@ additional-primitives-plan.md`의 "문서화 백로그" 절이 원자료)**:
가능 여부는 여전히 미정 — PreRef는 fire와 동시에 소진되는 1회성 가능 여부는 여전히 미정 — PreRef는 fire와 동시에 소진되는 1회성
pre-pass 참가자라 "취소"라는 개념 자체가 성립하는지부터 다시 볼 것). pre-pass 참가자라 "취소"라는 개념 자체가 성립하는지부터 다시 볼 것).
7. **왜 `Compute(fn, ...)`는 여러 의존성을 편하게 받고 `Effect`/`Observer`는
안 받는가** (2026-08-11 세션 원자료, `bind-system-plan.md` "`:Compute(fn,
...)` — 추가 의존성을 trailing args로 직접 받는 sugar" 절) — 겉보기엔
비일관적인 API 표면(하나는 React `useMemo`식 trailing deps sugar를 받고,
다른 둘은 명시적 `:With` 호출을 강제)이 실은 "sugar가 새 노드 생성 비용을
감추는가"라는 단일 원칙에서 갈라져 나온다는 게 소재 — `Compute`는 어차피
자기 결과를 담을 노드를 만들어야 해서 추가 구독을 얹는 게 공짜지만,
`Effect`/`Observer`는 자기 자신이 State 노드가 아니라 다중 의존성 병합에
진짜 새 노드가 필요해서 그 비용을 코드에 그대로 드러내는 쪽(`:With`
명시)을 택했다는 비교.
**publish 안 하는 것과의 경계**: 세션별 정정 이력, 조사 원자료(Fusion **publish 안 하는 것과의 경계**: 세션별 정정 이력, 조사 원자료(Fusion
반응 그래프 BFS 분석, quad2-try 죽은 코드 조사 등)는 quadnomicon에도 반응 그래프 BFS 분석, quad2-try 죽은 코드 조사 등)는 quadnomicon에도
안 들어감 — 그건 새 티어가 필요한 게 아니라 애초에 `.claude/` 내부 안 들어감 — 그건 새 티어가 필요한 게 아니라 애초에 `.claude/` 내부

View file

@ -2696,3 +2696,42 @@ Tween 제거, `None` 센티널 절 예시 갱신, Ref/Brand 절 문구 정정),
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인 **다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인
우선) — 이번 세션도 순수 설계 확정이라 M0 착수 우선순위 자체는 그대로. 우선) — 이번 세션도 순수 설계 확정이라 M0 착수 우선순위 자체는 그대로.
## 2026-08-11 세션 — `:Compute(fn, ...)` trailing-args sugar 확정,
`Effect`/`Observer`는 의도적으로 제외
사용자가 Vide의 암묵적 추적과 React 훅 규칙의 차이를 짚는 질문에서 출발해,
"React의 `useMemo(fn, deps)`처럼 `:With(...)` 없이 `:Compute(fn, a, b, c)`
바로 추가 의존성을 선언하면 더 편하지 않냐"는 제안으로 이어진 짧은 세션.
검토 끝에 확정, `base/bind-system-plan.md`(`:Compute` 절 신규 소절)/
`base/effect-plan.md`/`ROADMAP.md`(M3)/`research/documentation-content-map.md`
(quadnomicon 후보 7번)에 반영 완료:
- **`:Compute(fn, ...)`는 채택 — 진짜 공짜 sugar라는 게 사용자가 직접 밝힌
핵심 근거.** `:Compute` 호출은 원래도 결과를 담을 새 State 노드를 만들어야
하므로, 그 노드에 `self` 말고 `a,b,c`까지 구독(무효화 엣지)을 추가로 거는
건 이미 생기는 노드에 엣지만 얹는 것 — `:With(a,b,c):Compute(fn)`(노드
2개)보다 싼 노드 1개로 끝남. 이전에 기각됐던 `Store.Combine({a,b},
function(av,bv)...)`(포지셔널 값 언랩이라 타입 표기가 꼬였던 안)과는
달리 `fn(self)` lazy 핸들 시그니처를 그대로 유지하는 제안이라 그 기각
사유가 안 걸림.
- **`Effect(fn, ...)`/`state:Observer(fn, ...)`류 동일 sugar는 기각 —
사용자가 직접 구분.** Effect/Observer는 Compute와 달리 자기 자신이
결과를 담는 State 노드가 아닌 순수 leaf 소비자라, 의존성이 둘 이상이면
그걸 하나로 합칠 **새 노드**(`:With`가 만드는 것)가 실제로 필요함 —
이건 진짜 비용이 드는 지점이라, trailing args로 감추면 "이 줄이 새
노드/구독을 만든다"는 걸 코드만 보고 알 수 없게 됨. `:With`가 clone
빌더가 아니라 진짜 노드로 확정됐던 이유(2026-08-07 세 번째 세션,
"코드상의 호출 체인이 그래프 엣지와 1:1 대응돼야 quad-debug 그래프가
안 꼬임")와 정확히 같은 원칙 — 다중 의존성 Effect/Observer는
`Effect(fn, state:With(a,b,c))`처럼 `:With` 호출을 코드에 그대로 노출.
- **일반 원칙**: "trailing args sugar는 그게 정말 무료일 때만 붙인다" —
호출부가 이미 만들어야 하는 노드에 엣지만 얹는 경우(Compute)엔 sugar,
없던 노드를 새로 만들어야 하는 경우(Effect/Observer의 다중 의존성
병합)엔 sugar 없이 `:With`를 명시적으로 남긴다. `Compute`만 편해지고
`Effect`/`Observer`는 안 그런 게 겉보기엔 비일관적으로 보이지만 실은
이 하나의 원칙에서 나온 것이라는 게 quadnomicon 에세이 소재로 채택
(사용자 제안).
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, luau-test 결과 확인
우선) — 이번 세션도 순수 설계 확정이라 M0 착수 우선순위 자체는 그대로.

View file

@ -148,6 +148,12 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
- [ ] `Source.luau`/`State.luau`/`Store.luau` - [ ] `Source.luau`/`State.luau`/`Store.luau`
- [ ] `store.key` dot-access 타입 추론 확인 - [ ] `store.key` dot-access 타입 추론 확인
- [ ] `:Compute(fn, ...)` — trailing args로 추가 의존성 직접 받는 sugar
(2026-08-11 세션, `base/bind-system-plan.md` "`:Compute(fn, ...)`"
절) — `:With(...):Compute(fn)` 체인과 달리 노드 1개(Compute 노드
자신에 구독만 추가)로 끝나야 함, 새 노드 생성 없이 구현되는지 M0/M3
스파이크에서 확인. `Effect`/`Observer`는 대칭 sugar 없이 `:With` 명시
유지(의도적 비대칭, 같은 절 참고)
- [ ] `Blocker.luau`(`base/blocker-plan.md` 참고 — 여러 Source를 - [ ] `Blocker.luau`(`base/blocker-plan.md` 참고 — 여러 Source를
한꺼번에 바꿔도 파생값 재계산/재대입이 한 번만 되게 하는 primitive, 한꺼번에 바꿔도 파생값 재계산/재대입이 한 번만 되게 하는 primitive,
State와 밀접히 연관돼 있어 같은 마일스톤에서 개발) State와 밀접히 연관돼 있어 같은 마일스톤에서 개발)