decide(base): .value를 Ref 전용으로 확정, State/Source는 Get()만

State/Source의 값 읽기 접근자에서 .value(관용 표기)를 제거하고 :Get() 하나로
통일 — "관측해야 실체화된다" 원칙이 가장 날카롭게 느껴져야 할 지점에서
프로퍼티 문법이 그 느낌을 무디게 한다는 판단. .value 표기 자체는 폐기가
아니라 Ref 전용으로 좁혀짐(Ref는 lazy가 아니라 읽어도 계산이 안 트리거되므로
프로퍼티 문법이 정직함) — 이름 충돌 자체가 사라짐.

bind-system-plan.md/store-semantics.md/architecture.md/debug-tooling-plan.md
전체의 .value 언급을 :Get()으로 갱신, question.md의 관련 열린 질문은
완전히 해소되어 제거.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
qwreey 2026-08-07 00:40:57 +09:00
parent 53d43dc90b
commit 99ffea2f07
Signed by: qwreey
GPG key ID: D28DB79297A214BD
5 changed files with 34 additions and 32 deletions

View file

@ -204,7 +204,8 @@ push-invalidate(신호만)/
pull-recompute(`Get()` 시점) — Fusion식 eager 노드 없이도 다이아몬드 pull-recompute(`Get()` 시점) — Fusion식 eager 노드 없이도 다이아몬드
의존성 중복 재계산 문제가 풀림. State는 쓰기 대상이 아니고, 값을 쓰는 의존성 중복 재계산 문제가 풀림. State는 쓰기 대상이 아니고, 값을 쓰는
경로는 `source:Set(value)`(Source가 State보다 넓은 인터페이스를 가짐 — 경로는 `source:Set(value)`(Source가 State보다 넓은 인터페이스를 가짐 —
`.value`/`:Get()`/`:With`/`:Compute` 위에 `:Set`/`:Emit` 추가). 값 하나만 `:Get()`/`:With`/`:Compute` 위에 `:Set`/`:Emit` 추가; [정정, 2026-08-07]
읽기는 `:Get()` 하나로 통일 — `.value` 표기는 Ref 전용으로 좁혀짐). 값 하나만
다룰 땐 Store와 별개인 가벼운 `Source` 프리미티브를 독립적으로도 씀. 다룰 땐 Store와 별개인 가벼운 `Source` 프리미티브를 독립적으로도 씀.
`store.key` dot-access를 타입 추론 1급 경로로 삼는 것도 3차 라운드에서 `store.key` dot-access를 타입 추론 1급 경로로 삼는 것도 3차 라운드에서
정식 확정됨 — **더 이상 열린 질문 아님**, 남은 건 정확한 API 이름뿐. 정식 확정됨 — **더 이상 열린 질문 아님**, 남은 건 정확한 API 이름뿐.

View file

@ -404,7 +404,7 @@ Modifier와는 다른 부류).
```lua ```lua
local observer = state:Observer(function() local observer = state:Observer(function()
state.value state:Get()
end) end)
Frame { Frame {
@ -419,7 +419,7 @@ retract/Destroy되면 자동으로 정리됨.
무효화 신호 하나로 좁혀짐 — 값을 안 실어보내므로 저렴함" 원칙(아래 무효화 신호 하나로 좁혀짐 — 값을 안 실어보내므로 저렴함" 원칙(아래
"Store/State/Source 온톨로지" 절)이 그대로 적용됨: `fn`은 "뭔가 "Store/State/Source 온톨로지" 절)이 그대로 적용됨: `fn`은 "뭔가
바뀌었으니 다시 확인하라"는 신호만 받고 새 값 자체는 안 받음 — 바뀌었으니 다시 확인하라"는 신호만 받고 새 값 자체는 안 받음 —
위 예시처럼 `fn` 본문에서 `state.value`/`Get()`을 명시적으로 다시 위 예시처럼 `fn` 본문에서 `state:Get()`을 명시적으로 다시
읽어야 함. 자동으로 안 해주는 이유: 재계산이 진짜 필요한지가 다른 읽어야 함. 자동으로 안 해주는 이유: 재계산이 진짜 필요한지가 다른
`:With`한 값에 따라 갈리는 경우가 있어서(위 "포지셔널 인자 지양" 절의 `:With`한 값에 따라 갈리는 경우가 있어서(위 "포지셔널 인자 지양" 절의
`noprint` 예시처럼 계산 자체를 통째로 생략하고 싶을 수 있음) — `Get()` `noprint` 예시처럼 계산 자체를 통째로 생략하고 싶을 수 있음) — `Get()`
@ -532,7 +532,7 @@ read ...`처럼, State끼리 자유롭게 합성/파이핑 가능한 것이 최
아래 "State는 쓰기 대상이 아님" 절 이후 내용 및 `base/store-semantics.md` 아래 "State는 쓰기 대상이 아님" 절 이후 내용 및 `base/store-semantics.md`
"Source가 State를 만족함" 절 참고): "Source가 State를 만족함" 절 참고):
- **Source** — 실제 값이 존재하고 변경될 수 있는 단일 지점(v1의 "값의 근원"). - **Source** — 실제 값이 존재하고 변경될 수 있는 단일 지점(v1의 "값의 근원").
**구조적으로 State를 만족(단방향 호환)**`.value`/`:Get()`/`:With`/`:Compute` **구조적으로 State를 만족(단방향 호환)**`:Get()`/`:With`/`:Compute`
전부 지원 위에 `:Set(value)`/`:Emit()` 추가. 전부 지원 위에 `:Set(value)`/`:Emit()` 추가.
- **Store** — Source들의 이름 붙은 모음, 그 이상 아님. `store.a`처럼 키로 - **Store** — Source들의 이름 붙은 모음, 그 이상 아님. `store.a`처럼 키로
접근하면 **이미 만들어진 Source가 있으면 그대로 반환, 없으면 그 자리에서 접근하면 **이미 만들어진 Source가 있으면 그대로 반환, 없으면 그 자리에서
@ -557,7 +557,7 @@ Fusion식 eager 노드·생성순 정렬은 안 만듦**
- 신호를 받은 State는 자기 `invalid` 플래그만 세우고, 이미 `invalid`였다면 - 신호를 받은 State는 자기 `invalid` 플래그만 세우고, 이미 `invalid`였다면
그 아래로 더 전파하지 않는다 — 다이아몬드 의존성에서 중복 워크를 막는 그 아래로 더 전파하지 않는다 — 다이아몬드 의존성에서 중복 워크를 막는
장치(Vide가 저자 스스로 `todo.md`에 미해결로 남긴 문제의 해결책). 장치(Vide가 저자 스스로 `todo.md`에 미해결로 남긴 문제의 해결책).
- 실제 재계산은 `Get()`(또는 `.value` 인덱싱)이 호출되는 시점에만 일어남 — - 실제 재계산은 `:Get()`이 호출되는 시점에만 일어남 —
"필요할 때 계산" 원칙(사용자 확정). Fusion의 `timeliness="eager"` 노드/ "필요할 때 계산" 원칙(사용자 확정). Fusion의 `timeliness="eager"` 노드/
생성순 정렬 장치는 만들지 않음 — quad엔 그런 다단계 즉시 재계산이 필요한 생성순 정렬 장치는 만들지 않음 — quad엔 그런 다단계 즉시 재계산이 필요한
소비자가 없다는 판단. 유일하게 "즉시 반응해야 하는" 소비자는 store-bind 소비자가 없다는 판단. 유일하게 "즉시 반응해야 하는" 소비자는 store-bind
@ -571,14 +571,14 @@ Fusion식 eager 노드·생성순 정렬은 안 만듦**
**전역 원칙으로 명문화: "관측해야 실체화된다" (2026-08-04 세션)** **전역 원칙으로 명문화: "관측해야 실체화된다" (2026-08-04 세션)**
위 pull-recompute 규칙을 State 하나의 재계산 메커니즘으로만 읽지 말고, 위 pull-recompute 규칙을 State 하나의 재계산 메커니즘으로만 읽지 말고,
프로젝트 전역에 적용되는 원칙으로 명시함: **어떤 파생값도 `.value`/`Get()` 프로젝트 전역에 적용되는 원칙으로 명시함: **어떤 파생값도 `:Get()`으
직접 읽히기(관측) 전까지는 계산되지 않는다.** 이 원칙은 State 자체뿐 아니라, 직접 읽히기(관측) 전까지는 계산되지 않는다.** 이 원칙은 State 자체뿐 아니라,
State를 필드 값으로 담고 있는 다른 구조(예: `base/modifier-plan.md` State를 필드 값으로 담고 있는 다른 구조(예: `base/modifier-plan.md`
Modifier)에도 그대로 적용됨 — Modifier의 getter가 State 필드를 읽으면 그 Modifier)에도 그대로 적용됨 — Modifier의 getter가 State 필드를 읽으면 그
순간이 바로 관측이고, 그 순간 계산이 확정됨. 순간이 바로 관측이고, 그 순간 계산이 확정됨.
**주의 — 구조적 복사는 관측이 아님.** `table.clone`처럼 테이블 레퍼런스만 **주의 — 구조적 복사는 관측이 아님.** `table.clone`처럼 테이블 레퍼런스만
복사하는 연산은 안에 담긴 State 핸들을 그대로 옮길 뿐 `.value`/`Get()` 복사하는 연산은 안에 담긴 State 핸들을 그대로 옮길 뿐 `:Get()`
호출하지 않으므로 관측이 아니고, 계산을 트리거하지 않음. Modifier 체이닝 호출하지 않으므로 관측이 아니고, 계산을 트리거하지 않음. Modifier 체이닝
메소드가 `table.clone` 후 필드를 덮어쓰는 것(위 "Immutable 값 + clone 기반 메소드가 `table.clone` 후 필드를 덮어쓰는 것(위 "Immutable 값 + clone 기반
체이닝")과 이 원칙이 충돌하지 않는 이유가 바로 이것 — clone은 그저 참조 체이닝")과 이 원칙이 충돌하지 않는 이유가 바로 이것 — clone은 그저 참조
@ -625,23 +625,31 @@ Modifier처럼 플래튼하지 않는가"는 설계 근거를 알고 싶은 사
단점이 있었음 — self가 raw 값이면 `fn` 호출 전에 항상 self를 먼저 단점이 있었음 — self가 raw 값이면 `fn` 호출 전에 항상 self를 먼저
`Get()`해야 하므로, `fn` 내부 로직이 with한 다른 값을 보고 "이 경우엔 self `Get()`해야 하므로, `fn` 내부 로직이 with한 다른 값을 보고 "이 경우엔 self
계산 자체가 필요 없다"고 판단해도 이미 늦음(예: `:With(noprint)`이고 계산 자체가 필요 없다"고 판단해도 이미 늦음(예: `:With(noprint)`이고
`noprint.value == true`면 앞단 계산을 통째로 생략하고 싶은 경우). `noprint:Get() == true`면 앞단 계산을 통째로 생략하고 싶은 경우).
- **해결(사용자 확정)**: self도 raw 값이 아니라 **State 핸들 그 자체**를 - **해결(사용자 확정)**: self도 raw 값이 아니라 **State 핸들 그 자체**를
`fn`의 포지셔널 인자로 넘긴다 — `fn(self: State<T>)`, 내부에서 `fn`의 포지셔널 인자로 넘긴다 — `fn(self: State<T>)`, 내부에서
`self.value`(또는 `self:Get()`)를 실제로 읽을 때만 계산이 트리거됨. `self:Get()`을 실제로 읽을 때만 계산이 트리거됨. with한 값과 동일한
with한 값과 동일한 lazy 원칙을 self에도 그대로 적용 — 별도 lazy 원칙을 self에도 그대로 적용 — 별도 `ComputeWithout` 변형은
`ComputeWithout` 변형은 불필요, `Compute` 하나로 일관. 불필요, `Compute` 하나로 일관.
- `.value``Get()`을 감싼 읽기 전용 계산 속성(`base/lifecycle-pattern.md`의 - **[정정, 2026-08-07] `.value`는 State/Source에서 제외, `:Get()`만 지원.**
`Connected`와 동일한 "저장되는 필드가 아니라 계산된 속성" 패턴 재사용) — 이전엔 `Get()`을 감싼 읽기 전용 계산 속성(`base/lifecycle-pattern.md`의
`:Get()``.value` 둘 다 지원, `.value`가 관용적 표기. `Connected`와 동일한 "저장되는 필드가 아니라 계산된 속성" 패턴)으로
`.value`/`:Get()` 둘 다 지원하고 `.value`를 관용적 표기로 앞세웠으나,
"관측해야 실체화된다"는 원칙이 가장 날카롭게 느껴져야 할 지점에서
프로퍼티 문법이 그 느낌을 무디게 한다는 재검토 끝에 함수 호출
`:Get()` 하나로 좁힘 — `:Set()`과의 동사 짝도 자연스러움. `.value`
표기 자체는 폐기하지 않고 **Ref 전용으로 좁힘**(Ref는 lazy가 아니라
값을 읽어도 계산이 트리거되지 않으므로 프로퍼티 문법이 정직함 — 이
절 위쪽 "Ref 일반화" 절의 `.Value`가 그대로 유일한 존재가 됨, 이름
충돌 자체가 사라져 별도 표기 정리 불필요).
- 예시 갱신: `store "key1":With(store "key2"):Compute(function(key1) return - 예시 갱신: `store "key1":With(store "key2"):Compute(function(key1) return
key1.value + store.key2.value end)` — `key1`은 이제 raw 숫자가 아니라 key1:Get() + store.key2:Get() end)` — `key1`은 이제 raw 숫자가 아니라
State. State.
**State는 쓰기 대상이 아님 — 확정, Source는 독립 공개 프리미티브로 격상** **State는 쓰기 대상이 아님 — 확정, Source는 독립 공개 프리미티브로 격상**
- `.value`는 항상 읽기 전용. State에는 쓰기 API가 아예 없음. "State에 - `state:Get()`은 항상 읽기 전용. State에는 쓰기 API가 아예 없음. "State에
`.value = x`를 허용하면 다른 source에서 파생된 state에 직접 쓰기가 직접 쓰기 API를 허용하면 다른 source에서 파생된 state에 직접 쓰기가
가능해져 버린다"는 이전 우려는 이걸로 근본적으로 해소(그런 API 자체가 가능해져 버린다"는 이전 우려는 이걸로 근본적으로 해소(그런 API 자체가
없음). 없음).
- **[정정, 2026-08-06 후속 세션] 값을 쓰는 경로는 `store.key = value` - **[정정, 2026-08-06 후속 세션] 값을 쓰는 경로는 `store.key = value`

View file

@ -108,9 +108,12 @@ State를 만족하도록 만들고, RefSource라는 별도 타입은 폐기**하
**확정 방향**: **확정 방향**:
- **`Source<T>`가 구조적으로 `State<T>`를 만족(단방향 호환)** — State - **`Source<T>`가 구조적으로 `State<T>`를 만족(단방향 호환)** — State
자리엔 Source를 넣을 수 있지만 역은 안 됨(Svelte의 `Writable<T> extends 자리엔 Source를 넣을 수 있지만 역은 안 됨(Svelte의 `Writable<T> extends
Readable<T>`와 같은 모양). Source는 State가 주는 모든 것(`.value`, Readable<T>`와 같은 모양). Source는 State가 주는 모든 것(`:Get()`,
`:Get()`, `:With(...)`, `:Compute(fn)`) 위에 `:Set(value)`/`:Emit()`을 `:With(...)`, `:Compute(fn)`) 위에 `:Set(value)`/`:Emit()`을
추가로 가짐. 추가로 가짐([정정, 2026-08-07] `.value`는 State/Source에서 제외되고
`Get()`으로 통일됨, `.value` 표기는 Ref 전용으로 좁혀짐 — `base/
bind-system-plan.md` "`:With`/`:Compute` — self 인자도 lazy 핸들로
통일" 절 참고).
- **`:With`/`:Compute`는 Source에서도 항상 `State<U>`를 반환** — Source - **`:With`/`:Compute`는 Source에서도 항상 `State<U>`를 반환** — Source
자신을 변형하는 게 아니라, "Source의 State 뷰를 뽑아 그 위에 파이핑"하는 자신을 변형하는 게 아니라, "Source의 State 뷰를 뽑아 그 위에 파이핑"하는
것과 동치. 구현은 metatable `__index` 델리게이션(Source의 메소드 것과 동치. 구현은 metatable `__index` 델리게이션(Source의 메소드
@ -287,4 +290,4 @@ Source가 State 계약을 만족하는 이상 같은 이유(Modifier용 processo
연산.) 최종 형태는 `:With(...)`로 의존성을 모으고 연산.) 최종 형태는 `:With(...)`로 의존성을 모으고
`:Compute(fn)`으로 파생 State를 만드는 것으로 확정 — `Store.Combine({a,b}, `:Compute(fn)`으로 파생 State를 만드는 것으로 확정 — `Store.Combine({a,b},
fn)`류 포지셔널 인자 방식은 기각됨. 정확한 lazy 인자 규칙(self/with 값 둘 다 fn)`류 포지셔널 인자 방식은 기각됨. 정확한 lazy 인자 규칙(self/with 값 둘 다
State 핸들로 넘기고 `.value`를 실제로 읽을 때만 계산)은 `base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절 참고. State 핸들로 넘기고 `:Get()`을 실제로 읽을 때만 계산)은 `base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절 참고.

View file

@ -92,16 +92,6 @@ additional-primitives-plan.md`. 요지:
주기가 없다"는 기존 원칙과 이름이 충돌해 보일 수 있음), `Draw`(짧지만 주기가 없다"는 기존 원칙과 이름이 충돌해 보일 수 있음), `Draw`(짧지만
즉시모드 GUI 뉘앙스), `List`(중립적이나 메커니즘을 안 알려줌) — 아직 즉시모드 GUI 뉘앙스), `List`(중립적이나 메커니즘을 안 알려줌) — 아직
미정, `research/additional-primitives-plan.md` 1번 절 참고. 미정, `research/additional-primitives-plan.md` 1번 절 참고.
- **State/Source 읽기 접근자 — 함수명은 `Get()`으로 확정(2026-08-07),
`.value` 존치 여부만 남음**: `Get()` vs `Pull()``Get()`으로 확정 —
둘 다 "명시적 게터 호출"이라는 엔지니어링 기준에서 동급이라 승부처가
아니었고, 이미 문서 전체가 `Get()`을 쓰고 있어 바꿀 이유가 약함(`Finalize`는
앞서 quad의 "정리/해제" 어휘(Effect leaf-death cleanup, `retract`,
`Destroying`)와 충돌 위험으로 이미 기각). **아직 열려있는 건 `.value`
(Get()을 감싼 "관용적 표기" 계산 속성) 자체를 유지할지** — quad가
명문화한 "관측해야 실체화된다" 원칙을 프로퍼티 문법이 무디게 만드는 건
아닌지, 유지한다면 State/Source에도 둘지 아니면 진짜 non-lazy인 Ref에만
남길지.
- **"프로바이더"(3순위, 사소함)**: `base/module-lifecycle-plan.md` - **"프로바이더"(3순위, 사소함)**: `base/module-lifecycle-plan.md`
"provider"라고 불러온, `isHandlable`로 참여 여부를 결정하고 우선순위대로 "provider"라고 불러온, `isHandlable`로 참여 여부를 결정하고 우선순위대로
스캔되는 pluggable 참가자 개념 — 정확한 이름을 "provider"/"processor"/ 스캔되는 pluggable 참가자 개념 — 정확한 이름을 "provider"/"processor"/

View file

@ -257,7 +257,7 @@ trace 이벤트 페이로드는 처음부터 함수/클로저 없이 **순수
### 6. "관측해야 실체화된다" 원칙 — quad-debug 자신이 위반하면 안 됨 ### 6. "관측해야 실체화된다" 원칙 — quad-debug 자신이 위반하면 안 됨
`bind-system-plan.md`의 전역 원칙: 어떤 파생값도 `.value`/`Get()`으로 직접 `bind-system-plan.md`의 전역 원칙: 어떤 파생값도 `:Get()`으로 직접
읽히기 전까지 계산되지 않음. quad-debug UI가 트리뷰를 그리면서 모든 State를 읽히기 전까지 계산되지 않음. quad-debug UI가 트리뷰를 그리면서 모든 State를
자동으로 펼쳐 값을 미리 읽어버리면, 원래 필요 없었을 계산을 디버그 도구가 자동으로 펼쳐 값을 미리 읽어버리면, 원래 필요 없었을 계산을 디버그 도구가
유발하는 부작용이 생김 — **디버그 도구 자체도 lazy해야 함**: 사용자가 UI에서 유발하는 부작용이 생김 — **디버그 도구 자체도 lazy해야 함**: 사용자가 UI에서