decide(base): Compute 커링, state:Apply 확정, Effect Subscribe/Unsubscribe, 이중 바인딩 금지
- :Compute(fn)에도 Observer/Effect와 동일한 커링 권장 노트 추가 - state:Apply(factory) 확정 — ":With"/":Compute" 자동 등록 조합기였던 백로그안 기각, Modifier:Apply와 동일한 순수 체이닝 설탕으로 재정의 - EffectHandle:Subscribe()/:Unsubscribe() 신설 — leaf 없이 쓰는 독립 Effect 지원, :Unsubscribe()는 마지막 cleanup을 1회 트리거해야 함 - Observer/Effect 이중 바인딩(leaf 부착 + 수동 Subscribe) 금지 확정, Bound 플래그로 즉시 error - ROADMAP.md M3/question.md에 반영, effect-plan.md 오기 정정
This commit is contained in:
parent
b7ce11cf7f
commit
71729db816
5 changed files with 217 additions and 9 deletions
|
|
@ -515,6 +515,13 @@ Connect가 도는 숨은 churn 비용도 있음(Store Set은 dedup 안 함,
|
||||||
lazy State 핸들로 통일, 아래 "Store/State/Source 온톨로지" 절의 "`:With`/
|
lazy State 핸들로 통일, 아래 "Store/State/Source 온톨로지" 절의 "`:With`/
|
||||||
`:Compute`" 부분 참고).
|
`:Compute`" 부분 참고).
|
||||||
|
|
||||||
|
**`fn`을 커링 스타일로 짜는 것도 권장(2026-08-07 일곱 번째 세션)** —
|
||||||
|
`key:Compute(makeFormatter("ko-KR"))`처럼 팩토리가 실제 `fn`을 만들어
|
||||||
|
반환하는 패턴, Observer/Effect의 동일 관용구(아래 "`fn`을 커링 스타일로
|
||||||
|
짜는 것도 모듈화 관용구로 권장" 절, `base/effect-plan.md`)와 같은 결 —
|
||||||
|
`:Compute`가 원래부터 이 셋 중 제일 먼저 있던 자리라 뒤늦게 문서화된
|
||||||
|
것뿐, 새 결정이라기보다 이미 있던 패턴을 명문화한 것.
|
||||||
|
|
||||||
### `:Compute(fn)`의 선택적 두 번째 인자 — `previous` (무거운 파생 객체 재사용, 2026-08-06)
|
### `:Compute(fn)`의 선택적 두 번째 인자 — `previous` (무거운 파생 객체 재사용, 2026-08-06)
|
||||||
|
|
||||||
**배경**: `:Compute`의 결과가 그 자체로 무겁고 재생성 비용이 큰 엔진
|
**배경**: `:Compute`의 결과가 그 자체로 무겁고 재생성 비용이 큰 엔진
|
||||||
|
|
@ -630,14 +637,56 @@ retract/Destroy되면 자동으로 정리됨.
|
||||||
이 State가 계속 재계산되게만 강제하고 싶을 때 씀. 문서화만 확실히
|
이 State가 계속 재계산되게만 강제하고 싶을 때 씀. 문서화만 확실히
|
||||||
하면 별문제 없음(사용자 판단).
|
하면 별문제 없음(사용자 판단).
|
||||||
|
|
||||||
### 백로그(미확정) — `state:Apply(...)`: `:With`+`:Compute` 등록을 커링으로 자동화 (2026-08-07 여섯 번째 세션, 사용자 제안)
|
### `state:Apply(factory)` — Modifier와 동일한 순수 체이닝 설탕으로 확정 (2026-08-07 일곱 번째 세션)
|
||||||
|
|
||||||
Effect/Observer의 `fn` 커링 관용구 논의 중 나온 인접 아이디어 — `Modifier`의
|
**처음 제안됐던 "`:With`/`:Compute` 등록을 커링으로 자동화하는 조합기"
|
||||||
`:Apply(factory)`(팩토리 체이닝, `modifier-plan.md` 8번)와 비슷하게, 여러
|
방향은 기각됨 — 사용자가 재확인한 실제 의도는 그보다 훨씬 단순함.**
|
||||||
개를 커링으로 받아 알아서 `:With`/`:Compute` 등록을 대신 해주는
|
`Modifier:Apply(factory)`도 매번 새 값을 만들어내는 체이닝 설탕일 뿐이듯,
|
||||||
`state:Apply(...)` 같은 조합기가 있으면 편리할 수 있다는 제안. **지금 결정
|
State/Source도 `:With`/`:Compute`마다 새 노드가 나오는 같은 모양이라 —
|
||||||
필요 없음 — 백로그로만 기록.** 구체 시그니처/필요성 검증 없음, 나중에
|
`state:Apply(factory)`는 그냥 `factory(state)`를 메소드 체이닝 문법으로
|
||||||
`:With`/`:Compute` 관용구가 실제로 자주 반복되는 게 확인되면 다시 논의.
|
쓴 것뿐이고 그 이상의 계약은 없음(`Modifier:Apply`와 완전히 동일한
|
||||||
|
정의: `function(self, factory) return factory(self) end`).
|
||||||
|
|
||||||
|
- **동기**: 커링 팩토리 두 개 이상을 이미 있는 문법만으로 이으면 바깥에서
|
||||||
|
안으로 겹쳐 읽어야 하는 중첩 호출이 됨 — 실제 형태로 예를 들면,
|
||||||
|
```lua
|
||||||
|
-- Apply 없이: 안쪽(가장 최근에 만든 것)부터 거꾸로 읽어야 함
|
||||||
|
local capped = capAt(100)(withLocale(localeStore.locale)(rawScore))
|
||||||
|
|
||||||
|
-- state:Apply로: 왼쪽에서 오른쪽, 만든 순서 그대로 읽힘
|
||||||
|
local capped = rawScore
|
||||||
|
:Apply(withLocale(localeStore.locale))
|
||||||
|
:Apply(capAt(100))
|
||||||
|
```
|
||||||
|
팩토리가 세 개, 네 개로 늘어날수록 앞쪽 버전은 괄호 깊이와 읽는 방향이
|
||||||
|
코드 작성 순서와 반대로 꼬여 diff/리뷰에서 특히 안 좋음 — `:Apply`
|
||||||
|
버전은 각 줄이 "그다음 뭘 했는지"를 순서대로 나열하므로 Modifier
|
||||||
|
체이닝(`mod:FontSize(14):Apply(Boldify(10)):Apply(Italicify)`)과 읽는
|
||||||
|
방식이 완전히 통일됨. `:With`/`:Compute` 자체를 대신 호출해주는
|
||||||
|
자동화가 아니므로, 여전히 팩토리 본문 안에서 `:With`/`:Compute`를
|
||||||
|
직접 호출하는 건 팩토리 작성자 몫.
|
||||||
|
- **구현 비용 거의 0**: Modifier와 달리 State/Source는 제네릭 `__index`로
|
||||||
|
필드 setter를 즉석 합성하는 메커니즘이 없어서(고정된 메소드 표면만
|
||||||
|
존재), Modifier의 `Apply`처럼 "필드 이름으로 예약해야 하는" 충돌
|
||||||
|
자체가 없음 — 그냥 고정 메소드 하나 추가하는 것.
|
||||||
|
- **타입은 `factory: (State<T>) -> U): U`로 완전히 열어둠** — Modifier의
|
||||||
|
`Apply`는 `factory: (M) -> M`으로 같은 타입을 유지해야 체이닝이
|
||||||
|
이어지지만, State의 `:Apply`는 팩토리가 State가 아닌 값(예: 최종
|
||||||
|
요약된 plain 값)을 반환해 반응형 그래프를 벗어나는 탈출구로 쓰는 것도
|
||||||
|
막을 이유가 없음 — Modifier보다 오히려 더 자유로운 시그니처.
|
||||||
|
- **Source도 자동 포함**: Source가 State를 구조적으로 만족하는 기존
|
||||||
|
델리게이션(`__index`로 `:With`/`:Compute` 위임)에 `:Apply`도 그대로
|
||||||
|
얹히므로 별도 구현 불필요.
|
||||||
|
- **Effect/Observer/Compute의 `fn` 커링 권장(위 절들)과 같은 스레드지만
|
||||||
|
별개 기능** — 커링은 "`fn` 자체를 팩토리로 짜는 관용구" 권장이고,
|
||||||
|
`:Apply`는 그렇게 만든 팩토리를 체이닝 문법으로 적용하는 수단. 둘이
|
||||||
|
합쳐지면 `state:Apply(makeFormatter("ko-KR"))`처럼 자연스럽게 이어짐.
|
||||||
|
|
||||||
|
**Observer/Effect의 `:Subscribe()`/`:Unsubscribe()`는 이 절과 무관한
|
||||||
|
별개 주제** — 아래 새 절로 분리(이전에 이 헤더 아래 잘못 걸려 있던
|
||||||
|
문서 버그 수정, 내용 자체는 이미 확정된 것 그대로).
|
||||||
|
|
||||||
|
### Observer의 `:Subscribe()`/`:Unsubscribe()` — children 배열 밖 독립 구독 (2026-08-06 후속 세션)
|
||||||
|
|
||||||
**문제**: children 배열에 넣는 자동 라이프사이클 바인딩은 Observer가
|
**문제**: children 배열에 넣는 자동 라이프사이클 바인딩은 Observer가
|
||||||
"어딘가 leaf에 붙어있다"는 걸 전제함. 근데 흔한 실사용 패턴 하나가 이
|
"어딘가 leaf에 붙어있다"는 걸 전제함. 근데 흔한 실사용 패턴 하나가 이
|
||||||
|
|
@ -692,6 +741,50 @@ Effect/Observer의 `fn` 커링 관용구 논의 중 나온 인접 아이디어
|
||||||
객체를 mutate하고 그대로 돌려주는 것)지만 표면 문법은 비슷하게
|
객체를 mutate하고 그대로 돌려주는 것)지만 표면 문법은 비슷하게
|
||||||
체이닝 가능.
|
체이닝 가능.
|
||||||
|
|
||||||
|
### 이중 바인딩 금지 — leaf 부착과 `:Subscribe()`는 상호 배타적, `Bound` 플래그로 즉시 에러 (2026-08-07 일곱 번째 세션)
|
||||||
|
|
||||||
|
**규칙**: 같은 Observer/Effect 핸들 하나는 라이프사이클 바인딩 경로를
|
||||||
|
딱 하나만 가질 수 있음 — children 배열에 놓여 leaf에 자동 부착되거나
|
||||||
|
(위 weak table 경로) `:Subscribe()`로 수동 등록되거나(위 강참조
|
||||||
|
레지스트리 경로), 둘 중 하나만. **둘 다 동시에 걸리는 건 UB로 확정** —
|
||||||
|
이미 leaf에 부착된 핸들을 다시 `:Subscribe()`하는 것도, 이미
|
||||||
|
`:Subscribe()`한 핸들을 children 배열에 놓아 leaf로도 부착시키는 것도
|
||||||
|
둘 다 금지.
|
||||||
|
|
||||||
|
**UB를 조용한 오동작이 아니라 즉시 에러로 만든다** — 판별 비용이 사실상
|
||||||
|
0(불리언 필드 하나 확인)이라, 조용히 이상하게 동작하게 두는 것보다
|
||||||
|
바로 에러를 던져 버그를 그 자리에서 잡는 게 엔지니어링상 훨씬 쌈:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- :Subscribe() 진입부, children 배열 leaf 부착부 — 둘 다 진입 전 동일하게 확인
|
||||||
|
if self.Bound then
|
||||||
|
error("Observer/Effect가 이미 다른 경로로 바인딩됨 — leaf 부착과 :Subscribe()는 동시에 쓸 수 없음")
|
||||||
|
end
|
||||||
|
self.Bound = true
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Bound`는 가칭** — 용어 정리 라운드에서 최종 이름 재검토 대상
|
||||||
|
(`.claude/question.md`에 반영).
|
||||||
|
- 이 플래그는 어느 경로가 먼저 왔는지와 무관하게 "이미 바인딩됨"만
|
||||||
|
표시 — 두 진입점이 똑같이 확인/설정하므로 순서와 무관하게 대칭적으로
|
||||||
|
막힘.
|
||||||
|
- **`:Unsubscribe()`는 여전히 "어떤 경로로 바인딩됐든 그 계약을 끊는다"는
|
||||||
|
뜻으로 통일** — `Bound`가 어느 경로로 세워졌는지와 무관하게,
|
||||||
|
`:Unsubscribe()` 한 번으로 그 바인딩(leaf의 Destroying 연결이든 수동
|
||||||
|
강참조 등록이든)을 끝내고 최종 정리를 수행. 위 "`:Unsubscribe()`는
|
||||||
|
자동(리프) 케이스에도 동일하게 씀" 절과 정합 — 이중 바인딩 금지 규칙과
|
||||||
|
별개로, "단일 바인딩을 끊는" `:Unsubscribe()` 자체의 계약은 안 바뀜.
|
||||||
|
- **Effect도 동일 규칙 적용** — 내부적으로 Observer를 조합하는 경우든
|
||||||
|
`state` 없는 경우든 같은 `Bound` 게이트를 그대로 재사용
|
||||||
|
(`base/effect-plan.md`). 이전에 그 문서에 적어뒀던 "leaf 부착과
|
||||||
|
`:Subscribe()`를 동시에 쓰는 것도 안전"이라는 서술은 **이 규칙으로
|
||||||
|
대체(정정)** — 안전하게 지원하는 게 아니라 애초에 막아야 하는
|
||||||
|
조합이었음.
|
||||||
|
- **문서화 경고 대상(api/심화)**: "한 Effect/Observer 핸들을 children
|
||||||
|
배열에 놓았다면 그걸 다시 `:Subscribe()`하지 말 것, 반대도 마찬가지 —
|
||||||
|
두 경로를 동시에 쓰고 싶으면 각각 독립된 새 `Effect(...)`/
|
||||||
|
`state:Observer(...)` 호출로 따로 만들 것"을 명시할 것.
|
||||||
|
|
||||||
## Unix 파이프에서 영감 받은 스트림 지향 — 원래 의도, 해소됨
|
## Unix 파이프에서 영감 받은 스트림 지향 — 원래 의도, 해소됨
|
||||||
|
|
||||||
**배경**: quad는 원래 이 파이프라인/스트림 개념에서 영감을 받아 만들어짐.
|
**배경**: quad는 원래 이 파이프라인/스트림 개념에서 영감을 받아 만들어짐.
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,52 @@ end; lastConn = ... end)`) **Observer 자체**가 이걸 대신해줄 이유는
|
||||||
(Effect)으로 분리해 얹었을 뿐, Observer의 기본 계약(재실행 신호만, cleanup은
|
(Effect)으로 분리해 얹었을 뿐, Observer의 기본 계약(재실행 신호만, cleanup은
|
||||||
클로저로 직접)은 그대로 가볍게 유지됨.
|
클로저로 직접)은 그대로 가볍게 유지됨.
|
||||||
|
|
||||||
|
## `EffectHandle:Subscribe()`/`:Unsubscribe()` — leaf 없이 쓰는 독립 Effect (2026-08-07 일곱 번째 세션)
|
||||||
|
|
||||||
|
**동기**: 지금까지 Effect의 유일한 생애주기 경로는 children 배열의 leaf
|
||||||
|
부착뿐이었음 — leaf 없이 `Effect(fn)`/`Effect(fn, state)`를 호출하면
|
||||||
|
설치(1회 실행)는 되지만 반환된 `EffectHandle`엔 아무 인터페이스도 없어서
|
||||||
|
cleanup을 트리거할 방법이 없는 막다른 길이었음. `state:Observer(fn)`가
|
||||||
|
이미 `:Subscribe()`/`:Unsubscribe()`(위 bind-system-plan.md 절)로 "children
|
||||||
|
배열 밖, 모듈/스크립트 레벨에서 독립적으로 켜고 끄는" 경로를 갖고 있는데,
|
||||||
|
Effect도 모듈/스크립트 사이드 이펙트(백그라운드 시스템, non-UI 코드가
|
||||||
|
quad의 반응형 그래프/cleanup 인체공학만 재사용하는 경우)로 쓰일 수 있어서
|
||||||
|
같은 결로 필요 — `Effect`도 leaf 없이 독립적으로 켜고 끌 수 있어야 함.
|
||||||
|
|
||||||
|
**확정**: `EffectHandle`에도 `:Subscribe()`/`:Unsubscribe()` 추가, 둘 다
|
||||||
|
`self` 반환(Observer와 동일한 fluent 대칭).
|
||||||
|
|
||||||
|
- **`:Subscribe()`** — Observer가 쓰는 것과 같은 강참조 레지스트리에
|
||||||
|
자신(또는 `state` 있는 경우 내부 Observer)을 등록 — 새 메커니즘 아님,
|
||||||
|
기존 레지스트리 재사용. 이후 로컬 변수로 참조를 안 들고 있어도 계속
|
||||||
|
살아있음(Observer와 동일 관용구).
|
||||||
|
- **`:Unsubscribe()`는 Observer의 것을 그냥 위임하지 않는다 — Effect
|
||||||
|
계층에서 의미가 확장됨.** Observer의 `:Unsubscribe()`는 "미래 재실행만
|
||||||
|
끊는다"(Observer 자체엔 정리할 상태가 없음)로 충분하지만, Effect의
|
||||||
|
계약은 "생애주기가 끝나는 시점에 마지막 cleanup이 정확히 1회 호출된다"
|
||||||
|
이고 leaf 사망은 그 "끝"의 신호 중 하나일 뿐이라, `:Unsubscribe()`도
|
||||||
|
동일하게 "지금 끝났다"는 신호로 취급해야 계약이 일관됨:
|
||||||
|
1. `state`가 있으면 내부 Observer도 `:Unsubscribe()`해서 향후 재실행을
|
||||||
|
끊고,
|
||||||
|
2. **직전(또는 유일한) cleanup을 정확히 1회 호출** — leaf가 죽을 때
|
||||||
|
하던 것과 정확히 같은 이벤트를 수동으로 앞당기는 것.
|
||||||
|
3. **idempotent, 그리고 이후 leaf가 실제로 죽어도 cleanup이 중복
|
||||||
|
호출되면 안 됨** — 새 메커니즘 불필요, Observer가 이미 확정해둔
|
||||||
|
"`Subscribed` 필드 우선 liveness 체크"가 자동(리프)/수동(Unsubscribe)
|
||||||
|
두 경로를 하나의 게이트로 OR 묶어주므로 여기 그대로 얹힘.
|
||||||
|
- **`state` 없는 mount-only Effect엔 특별한 분기 불필요** — install은 이미
|
||||||
|
`Effect(fn)` 호출 시점에 끝나 있으므로, `:Unsubscribe()`는 그냥 "지금
|
||||||
|
leaf-사망 cleanup을 수동으로 트리거"하는 것과 완전히 동치.
|
||||||
|
- **leaf 부착과 `:Subscribe()`를 동시에 쓰는 건 UB — 정정(2026-08-07
|
||||||
|
일곱 번째 세션 후속)**: 처음엔 "같은 liveness 게이트를 공유하니
|
||||||
|
동시에 써도 안전"으로 적었으나, 애초에 한 핸들은 라이프사이클 바인딩
|
||||||
|
경로를 하나만 가져야 한다는 게 맞는 방향이라 판단이 뒤집힘 — 상세
|
||||||
|
규칙과 `Bound` 플래그 기반 즉시-에러 메커니즘은
|
||||||
|
`base/bind-system-plan.md`의 "이중 바인딩 금지" 절 참고. leaf 부착
|
||||||
|
**후** `:Unsubscribe()`로 조기 해제하는 것(위 "Observer의
|
||||||
|
`:Unsubscribe()`는 자동 케이스에도 동일하게 씀" 패턴)은 여전히 정상 —
|
||||||
|
금지되는 건 leaf 부착과 `:Subscribe()`를 **같이** 쓰는 것뿐.
|
||||||
|
|
||||||
## 해결됨 — Effect/Observer 관계 (2026-08-07 여섯 번째 세션, 이전 미해결 절 대체)
|
## 해결됨 — Effect/Observer 관계 (2026-08-07 여섯 번째 세션, 이전 미해결 절 대체)
|
||||||
|
|
||||||
**과거 미해결이었던 두 질문 모두 확정**:
|
**과거 미해결이었던 두 질문 모두 확정**:
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,10 @@ context-rejected.md`. 아래는 그중 **아직 실제로 열려있는 것만**
|
||||||
Source 판별 predicate 세 개의 이름 — 동작은 전부 확정(`base/
|
Source 판별 predicate 세 개의 이름 — 동작은 전부 확정(`base/
|
||||||
modifier-plan.md` 9번, `base/bind-system-plan.md`의 `isState` 절),
|
modifier-plan.md` 9번, `base/bind-system-plan.md`의 `isState` 절),
|
||||||
이름만 다른 가칭들과 같이 용어 정리 라운드에서 재검토.
|
이름만 다른 가칭들과 같이 용어 정리 라운드에서 재검토.
|
||||||
|
- **`Bound`(3순위, 사소함, 2026-08-07 일곱 번째 세션 추가)**: Observer/
|
||||||
|
Effect 핸들이 leaf 부착과 `:Subscribe()` 중 이미 어느 한쪽으로
|
||||||
|
바인딩됐는지 표시하는 내부 플래그 이름(`base/bind-system-plan.md`
|
||||||
|
"이중 바인딩 금지" 절) — 동작은 확정, 이름만 가칭.
|
||||||
- **"프로바이더"(3순위, 사소함)**: `base/module-lifecycle-plan.md`가
|
- **"프로바이더"(3순위, 사소함)**: `base/module-lifecycle-plan.md`가
|
||||||
"provider"라고 불러온, `isHandlable`로 참여 여부를 결정하고 우선순위대로
|
"provider"라고 불러온, `isHandlable`로 참여 여부를 결정하고 우선순위대로
|
||||||
스캔되는 pluggable 참가자 개념 — 정확한 이름을 "provider"/"processor"/
|
스캔되는 pluggable 참가자 개념 — 정확한 이름을 "provider"/"processor"/
|
||||||
|
|
|
||||||
57
CLAUDE.md
57
CLAUDE.md
|
|
@ -930,9 +930,64 @@ setter를 단발로 직접 호출하는 흔한 경로는 여전히 mutable이라
|
||||||
- **백로그로만 기록, 결정 안 함**: `state:Apply(...)`처럼 여러 개를 커링으로
|
- **백로그로만 기록, 결정 안 함**: `state:Apply(...)`처럼 여러 개를 커링으로
|
||||||
받아 `:With`/`:Compute` 등록을 자동화하는 조합기 아이디어(사용자 제안,
|
받아 `:With`/`:Compute` 등록을 자동화하는 조합기 아이디어(사용자 제안,
|
||||||
`Modifier:Apply`의 State판 대응물) — `base/bind-system-plan.md`에 백로그
|
`Modifier:Apply`의 State판 대응물) — `base/bind-system-plan.md`에 백로그
|
||||||
절로만 남김, 시그니처/필요성 미검증.
|
절로만 남김, 시그니처/필요성 미검증. **(2026-08-07 일곱 번째 세션에서
|
||||||
|
이 방향 자체가 기각되고 훨씬 단순한 형태로 확정됨 — 아래 참고.)**
|
||||||
- 이걸로 `question.md` 0번(추가 프리미티브 논의)의 열린 항목은 "키 기반
|
- 이걸로 `question.md` 0번(추가 프리미티브 논의)의 열린 항목은 "키 기반
|
||||||
동적 컬렉션 재조정" 하나만 남음.
|
동적 컬렉션 재조정" 하나만 남음.
|
||||||
|
|
||||||
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 이미
|
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 이미
|
||||||
설계된 것의 세부 마무리라 M0 착수 우선순위 자체는 그대로.
|
설계된 것의 세부 마무리라 M0 착수 우선순위 자체는 그대로.
|
||||||
|
|
||||||
|
## 2026-08-07 일곱 번째 세션 — `:Compute` 커링, `state:Apply` 확정(백로그안 기각), Effect `:Subscribe`/`:Unsubscribe` 신설, 이중 바인딩 금지
|
||||||
|
|
||||||
|
짧은 대화형 세션, 네 가지를 순서대로 처리 — 전부 `base/bind-system-plan.md`/
|
||||||
|
`base/effect-plan.md`/`ROADMAP.md`/`question.md`에 반영 완료:
|
||||||
|
|
||||||
|
1. **`:Compute(fn)`에도 커링 권장 노트 추가.** 여섯 번째 세션에서 Observer/
|
||||||
|
Effect의 `fn`에만 문서화됐던 "팩토리가 실제 `fn`을 만들어 반환하는
|
||||||
|
커링 스타일 권장"이 `:Compute`엔 빠져 있었음 — 같은 결이라 자연스럽게
|
||||||
|
확장, `bind-system-plan.md` "`:With`+`:Compute`" 절에 추가.
|
||||||
|
2. **`state:Apply(factory)` 확정 — 원래 백로그였던 "`:With`/`:Compute`
|
||||||
|
등록을 커링으로 자동화하는 조합기" 방향은 기각.** 사용자가 재확인한
|
||||||
|
실제 의도는 훨씬 단순함: `Modifier:Apply`와 똑같이 `factory(self)`를
|
||||||
|
체이닝 문법으로 부르는 순수 설탕(`function(self, factory) return
|
||||||
|
factory(self) end`) — `fnb(c,d)(fn(a,b)(state))`처럼 팩토리를 안에서
|
||||||
|
밖으로 겹쳐 읽어야 하는 중첩을 `state:With(a,b):Compute(fn(a,b))
|
||||||
|
:Apply(fnb(c,d))`로 펴는 게 유일한 목적. 구현 비용 거의 0(State는
|
||||||
|
Modifier와 달리 제네릭 `__index` 필드 setter 합성이 없어 이름 예약
|
||||||
|
충돌도 없음), 타입은 `factory: (State<T>) -> U): U`로 Modifier보다
|
||||||
|
더 열어둠(팩토리가 State 밖 plain 값을 반환해 반응형 그래프를 벗어나는
|
||||||
|
것도 허용). Source는 기존 `:With`/`:Compute` 델리게이션에 얹혀 자동
|
||||||
|
포함. `bind-system-plan.md` "`state:Apply(factory)`" 절, 구체 전/후
|
||||||
|
코드 예시까지 반영. 부수적으로 같은 헤더 아래 잘못 걸려 있던 Observer
|
||||||
|
`:Subscribe`/`:Unsubscribe` 내용(무관한 주제)을 별도 절로 분리하는
|
||||||
|
문서 버그도 수정.
|
||||||
|
3. **`EffectHandle:Subscribe()`/`:Unsubscribe()` 신설.** 지금까지 Effect의
|
||||||
|
유일한 생애주기 경로는 children 배열 leaf 부착뿐이라, leaf 없이 쓰는
|
||||||
|
모듈/스크립트 레벨 사이드 이펙트(백그라운드 시스템 등)엔 반환된
|
||||||
|
`EffectHandle`이 막다른 길이었음 — Observer가 이미 가진 `:Subscribe`/
|
||||||
|
`:Unsubscribe`와 같은 결로 확정. **핵심 주의점**: Effect의
|
||||||
|
`:Unsubscribe()`는 Observer의 것을 그냥 위임하면 안 됨 — Observer의
|
||||||
|
계약은 "미래 재실행만 끊는다"로 충분하지만, Effect의 계약은 "생애주기가
|
||||||
|
끝나는 시점에 마지막 cleanup이 정확히 1회 호출된다"이고 leaf 사망은
|
||||||
|
그 "끝"의 신호 중 하나일 뿐이라, `:Unsubscribe()`도 동일하게 "지금
|
||||||
|
끝났다"는 신호로 취급해 마지막 cleanup을 트리거해야 계약이 일관됨(leaf
|
||||||
|
가 살아있어도 마찬가지). idempotent 보장은 기존 `Subscribed` 필드
|
||||||
|
liveness 체크 재사용으로 공짜. `base/effect-plan.md` 신규 절.
|
||||||
|
4. **Observer/Effect 이중 바인딩 금지 — `Bound`(가칭) 플래그로 즉시
|
||||||
|
`error`.** 처음엔 "leaf 부착과 `:Subscribe()`를 동시에 써도 같은
|
||||||
|
liveness 게이트를 공유하니 안전"이라고 적었으나, 사용자가 애초에 한
|
||||||
|
핸들은 라이프사이클 바인딩 경로를 하나만 가져야 한다고 정정 — 동시
|
||||||
|
바인딩은 UB로 확정하되, 판별 비용이 사실상 0(불리언 필드 하나)이라
|
||||||
|
조용한 오동작 대신 그 자리에서 `error`를 던지는 쪽으로 결정
|
||||||
|
(엔지니어링 비용 대비 디버깅 이득이 명확). 두 진입점(`:Subscribe()`
|
||||||
|
호출부, children 배열 leaf 부착부)이 똑같이 확인/설정하는 대칭적 게이트
|
||||||
|
— 순서 무관. `bind-system-plan.md` "이중 바인딩 금지" 절 신설,
|
||||||
|
`effect-plan.md`의 3번 항목 서술은 이 규칙으로 대체(정정 표시 남김).
|
||||||
|
|
||||||
|
**부수 정리**: `ROADMAP.md` M3에 `state:Apply`/Effect `:Subscribe`·
|
||||||
|
`:Unsubscribe`/이중 바인딩 금지 체크박스 추가. `question.md`에 `Bound`
|
||||||
|
이름을 용어 정리 대상(3순위)으로 추가.
|
||||||
|
|
||||||
|
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터) — 이번 세션도 순수
|
||||||
|
설계 확정이라 M0 착수 우선순위 자체는 그대로.
|
||||||
|
|
|
||||||
12
ROADMAP.md
12
ROADMAP.md
|
|
@ -67,13 +67,23 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
|
||||||
- [ ] `Blocker.luau`(`base/blocker-plan.md` 참고 — 여러 Source를
|
- [ ] `Blocker.luau`(`base/blocker-plan.md` 참고 — 여러 Source를
|
||||||
한꺼번에 바꿔도 파생값 재계산/재대입이 한 번만 되게 하는 primitive,
|
한꺼번에 바꿔도 파생값 재계산/재대입이 한 번만 되게 하는 primitive,
|
||||||
State와 밀접히 연관돼 있어 같은 마일스톤에서 개발)
|
State와 밀접히 연관돼 있어 같은 마일스톤에서 개발)
|
||||||
|
- [ ] `state:Apply(factory)`(`base/bind-system-plan.md` "`state:Apply(factory)`"
|
||||||
|
절, 2026-08-07 일곱 번째 세션) — `factory(self)`를 체이닝 문법으로
|
||||||
|
부르는 순수 설탕, `factory: (State<T>) -> U): U`로 열린 타입. Source도
|
||||||
|
기존 `:With`/`:Compute` 델리게이션에 얹혀 자동 포함
|
||||||
- [ ] `state:Observer(fn)` — children 배열 leaf 참가자, **등록 즉시 1회
|
- [ ] `state:Observer(fn)` — children 배열 leaf 참가자, **등록 즉시 1회
|
||||||
실행 확정**(`base/bind-system-plan.md`의 Observer 절), `isObserver`
|
실행 확정**(`base/bind-system-plan.md`의 Observer 절), `isObserver`
|
||||||
판별자, canExecute 게이팅, `:Subscribe()`/`:Unsubscribe()`
|
판별자, canExecute 게이팅, `:Subscribe()`/`:Unsubscribe()`
|
||||||
- [ ] `Effect(fn, state?)`(`base/effect-plan.md`) — `state` 생략 시 설치
|
- [ ] `Effect(fn, state?)`(`base/effect-plan.md`) — `state` 생략 시 설치
|
||||||
1회+leaf 사망 시 확정 정리, `state` 지정 시 내부적으로
|
1회+leaf 사망 시 확정 정리, `state` 지정 시 내부적으로
|
||||||
`state:Observer(...)`를 조합해 재실행+cleanup 체이닝(React
|
`state:Observer(...)`를 조합해 재실행+cleanup 체이닝(React
|
||||||
`useEffect` 동형). Observer 구현 이후에 착수(의존 관계)
|
`useEffect` 동형). Observer 구현 이후에 착수(의존 관계).
|
||||||
|
`EffectHandle:Subscribe()`/`:Unsubscribe()`도 추가(leaf 없이 쓰는
|
||||||
|
모듈/스크립트 레벨 Effect) — `:Unsubscribe()`는 Observer와 달리
|
||||||
|
마지막 cleanup을 1회 트리거해야 함(2026-08-07 일곱 번째 세션)
|
||||||
|
- [ ] Observer/Effect 이중 바인딩 금지 — `Bound`(가칭) 플래그로 leaf 부착과
|
||||||
|
`:Subscribe()`가 동시에 걸리면 즉시 `error`(`base/bind-system-plan.md`
|
||||||
|
"이중 바인딩 금지" 절, 2026-08-07 일곱 번째 세션)
|
||||||
- [ ] mock 대상 테스트
|
- [ ] mock 대상 테스트
|
||||||
|
|
||||||
## M4 — 첫 end-to-end 반응형 업데이트
|
## M4 — 첫 end-to-end 반응형 업데이트
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue