座標即內容:受約束組合空間的雙射枚舉與可導航生成宇宙
—— 以「詩雲 / Poetry Cloud」為例的技術重建與通用化
本文性質:技術方法論論文,目的是把一類技術從一個具體網頁中抽象出來、形式化、並寫到能照著從零重建的精度。
clean-room 聲明:本文只提取方法(演算法與不變式)——方法、數學、演算法不受著作權保護。本文不引用、不重製對方的原始碼、字表資料、品牌或任何受保護的具體表達。所有命名、結構、公式均為本文獨立重述,供獨立重新實作之用。(可查證:對方的字表 DATA 本就不在前端程式碼內,而是另以外部 JSON 載入;詩人真作資料表亦然。本文未取得、亦未重述其內容。)
定位:承 paradigms/ 的 MSSP 與「從尋找到生成」範式;本技術是「生成 > 儲存/尋找」的一個可運行具體化。
0. 摘要
存在一類網頁,它把「所有可能的詩」做成一座可以用滑鼠飛進去的三維星雲。它那座「無限生成星雲」不靠資料庫、不把任何一首『生成的』詩存在任何地方(少數策展真作另從靜態資料載入,見 §11.2,不影響生成層的零儲存);它的網址列裡那一串近百位的數字,本身就是一首詩的完整座標。樸素爬蟲抓到的是一個空殼,因為內容不在 HTML 裡——內容是瀏覽器拿著那個數字,從一個雙射枚舉當場算出來的。
把這件事拆開,會發現它不是「一個關於詩的網站」,而是一個通用模式的實例。本文將這個模式形式化為四個構件:
- 內容即索引:用雙射數系把「一個物件」與「一個大整數」一一對應;
- 受約束子空間的排名/反排名:當合法物件只佔組合空間的極小一塊(如格律詩的平仄與押韻),對這個受約束子集建立
整數 ↔ 物件的雙射——這是全技術的皇冠; - 座標 ↔ 索引的可逆綁定:用確定性空間雜湊,把每個索引固定散佈到三維空間的一點,反過來也能由空間位置取回索引;
- URL 即狀態:把索引寫進網址,於是「內容定址、零後端、可分享、天然防爬」同時成立。
本文逐一形式化這四個構件、證明其關鍵不變式、抽象出通用的「X 之雲」介面、給出可照著重建的 MSSP 模組藍圖與 Knuth 閘門測試計畫,最後誠實對照它的邊界(訊號淹沒、可發現性、技術與財產的界線)。
1. 緣起:從一個「網頁比賽」到一類技術
原作把它當成一個有趣的網頁作品。但作品只是載體,真正被它一次到位地縫合起來的,是幾條本來各自存在、卻很少被合到一起用的線索:
- 波赫士《巴別圖書館》:一座包含所有可能書籍的圖書館。它的哲學困境是「一切都在裡面,但你找不到任何有意義的東西」。
- 劉慈欣《詩雲》:一個技術奇點文明為了寫出超越李白的詩,窮舉了所有可能的漢字排列,把整個太陽系變成儲存裝置——卻發現「窮舉出了所有好詩,卻無法把它們從垃圾裡認出來」。
- 組合物件的排名/反排名(ranking / unranking):計算組合學裡一個古典工具——在一個有限集合上建立
{0,1,…,|S|-1} ↔ S的雙射,於是可以「給編號取物件」與「給物件取編號」。 - 內容定址(content addressing):當內容能由其本身的標識完全決定,標識就不再是「指向內容的指標」,而就是內容。
把這四條合起來,就得到本文要重建的東西。《詩雲》小說裡做不到的「從垃圾裡認出好詩」這一步,這個技術誠實地沒有解決——但它解決了前一半:讓整個空間可被定址、可被導航、可被分享,且零儲存。這一半本身就極有價值,而且與「詩」完全無關,可以套到任何結構化的組合內容上。
1.1 歷史脈絡(站在哪些肩膀上)
這個技術不是憑空的,它是幾個成熟領域第一次被縫在同一個介面背後:
- 巴別圖書館的可運行版本:Jonathan Basile 的 Library of Babel 網站早已示範「用座標當場生成全部可能的英文文本頁」,核心也是「頁面內容 = 座標的確定性函數」。本技術可視為它的「加約束 + 三維可導航 + 漢語格律」進階版——關鍵升級正是 §4 的「受約束子空間」。
- 組合物件的排名/反排名:這是計算組合學的古典工具(Nijenhuis–Wilf《Combinatorial Algorithms》、Kreher–Stinson《Combinatorial Algorithms: Generation, Enumeration, and Search》系統化了排列、組合、子集等的 rank/unrank)。本技術把它從「排列組合」推到「受平仄/押韻約束的字串」。
- 受文法約束語言的排名:當約束更強(合法物件 = 某個自動機/文法接受的語言),就進入 Goldberg–Sipser 一系「ranking regular / context-free languages」的主題。§9.3 的難度光譜正沿著這條線往上爬。
- 內容定址與確定性生成:從 Git/IPFS 的「內容即位址」,到 demoscene/程序生成(procedural generation)用「種子 → 確定性世界」——本技術把「種子」換成「內容本身的編號」,於是種子不再指向內容,種子就是內容。
換句話說:rank/unrank(給編號 ⇄ 給物件)是引擎,內容定址(編號即網址)是傳動,確定性散佈(編號 → 座標)是把它變成可飛行空間的車輪。 三者單獨都不新;合起來對準「組合內容」這個目標,就成了一台新車。
2. 全景:四個構件與一句話原理
一句話原理:把內容空間S編號成整數區間[0, |S|),用可逆雜湊把每個編號釘在三維空間的固定一點,再把編號寫進網址——於是「飛行 = 瀏覽編號空間」「網址 = 內容」「無限 = 不儲存」。
┌─────────────────────────────────────────────┐
URL ?p=<大整數>│ i ∈ [0, |S|) ← 內容定址 / 可分享 │
└───────────────┬─────────────────────────────┘
│ unrank
▼
┌────────────────────────────────────────────┐
│ 雙射枚舉(兩套並存) │
│ ①自由數系:URL 的 i ⇄ object(變長自由詩) │
│ ②受約束子集 S_valid:位置→只逛「合格律」詩之引擎 │
└───────────────┬───────────────┬──────────────┘
render │ │ index (Vi/rank)
▼ ▼
┌──────────────┐ ┌────────────────────────┐
│ 構件① 內容 │ │ 構件③ 座標 ⇄ 索引 │
│ 即索引(數系) │ │ pos = scatter(i) │
│ object↔digits│ │ i = spatialHash(pos) │
└──────────────┘ └───────────┬────────────┘
▼
3D 星雲渲染(飛行/環視)
兩套雙射並存(重要釐清):原作的可分享 URL(?p=)攜帶的是構件①的「自由形式」編號(變長雙射數系,over Σ\*),載入時被解碼成自由詩。構件②的「受約束子集」雙射(§4)在原作裡是另一個用途——讓使用者只在「合格律」的詩之間飛(由空間位置經locate取一個合法編號、再用受約束unrank解出一首必定合律的詩),以及計算顯示用的lushiIndex詮釋資料。URL 並未攜帶 §4 的受約束 rank。 重建時你可以選擇讓 URL 改攜受約束 rank(見 §6 註)——那是相對原作的乾淨改良,但要先知道原作不是這樣做的。
四構件彼此正交,可分別替換:換數系/約束就換了「什麼是一首詩」;換散佈雜湊就換了空間佈局;換渲染就換了呈現媒介;URL 層幾乎不變。下面逐件展開。
3. 構件①:內容即索引——雙射數系與混合基數枚舉
3.1 自由枚舉(uniform 混合基數)
設字表(lexicon)是一個有序有限集合 Σ,N = |Σ|,每個字元有唯一序號 0 … N−1。一首「定長」詩就是長度 L 的序號串 c = (c_0, …, c_{L−1}),c_k ∈ [0, N)。
- 反排名 unrank(整數 → 物件):把整數
i ∈ [0, N^L)做L位定基數N展開:
for k = L-1 downto 0: c_k = i mod N; i = i div N
- 排名 rank(物件 → 整數):
i = (((c_0)·N + c_1)·N + … )·N + c_{L-1}。
這就是把「一首詩」看成一個 N 進位的 L 位數。空間大小 |S| = N^L。以 N≈2500(常用字級)為例:
| 形式 | 行×字 | L | 空間 N^L 量級 | |---|---|---|---| | 五絕 | 4×5 | 20 | ~10^68 | | 七絕 | 4×7 | 28 | ~10^95 | | 五律 | 8×5 | 40 | ~10^136 | | 七律 | 8×7 | 56 | ~10^190 |
可觀測宇宙的原子數約 10^80。七律的編號用盡了,宇宙的原子都不夠當紙。 但我們一張紙都不用——詩不被儲存,只被「算」。
3.2 雙射數系(bijective base:解決變長與前導零歧義)
定長很乾淨,但若要枚舉變長內容(自由詩、含換行、空白),普通進位制會出問題:0、00、000 在普通 N 進位下都對應同一個值,於是「字串 ↔ 整數」不再是雙射(前導零歧義、空串無法唯一編碼)。
解法是雙射數系(bijective numeration):基數 b,但每位的數字取值是 1 … b(而不是 0 … b−1)。此時非負整數與「有限字串」之間是嚴格雙射:
- rank:
i = (((d_0)·b + d_1)·b + …)·b + d_{m-1},其中d_k ∈ {1,…,b}。 - unrank:
digits = []
while i > 0: i = i - 1; digits.unshift(i mod b); i = i div b // 得到 0…b-1,外層再 +1 對映回 1…b
實作上,把「字元序號 0…N−1」與一個「換行/分隔 token」一起塞進基數 b = N+1 的雙射數系(每個符號映到數字 符號序號+1 ∈ {1,…,N+1}),就得到一個能唯一編碼任意長度、任意分行的自由詩的雙射整數。網址裡那串近百位數,正是這種「自由形式」的編號。
要點:定長 → 普通混合基數,變長 → 雙射數系。兩者都給你整數 ↔ 物件的雙射,差別只在邊界處理。
3.3 字表:把符號變成可索引的有序集
數系只認「序號」,不認「字」。所以需要一張字表 Σ:一個固定順序的符號陣列,序號就是它在陣列裡的位置。字表是整個系統唯一需要「決定一次」的資料;它一旦固定,所有編號的意義就固定了。字表是配置,不是內容——換一張字表(換語言、換符號集),整座宇宙就換了內容,但程式碼一行不動。
在 clean-room 重建裡,字表必須自己重建(用你自己選的字集與排序),不能沿用對方的——因為字表是它的具體資料表達(受保護),而「用一張有序字表當基底」這個方法不受保護。
3.4 一個最小的數系往返範例
把抽象落到地面。設字表只有四個字 Σ = [天, 地, 玄, 黃],N = 4,序號 天=0, 地=1, 玄=2, 黃=3。
- 定長(L=2,普通混合基數):編號
i = 11。
c_1 = 11 mod 4 = 3 → 黃 ; i = 11 div 4 = 2
c_0 = 2 mod 4 = 2 → 玄
得「玄黃」。反向:rank(玄黃) = 2·4 + 3 = 11。往返一致。空間大小 4^2 = 16,編號 0…15 正好對應十六首「兩字詩」。
- 變長(雙射數系,基數
b = N = 4,數字取1…4;字元序號s對映數字s+1):要能唯一編碼「空」「一字」「兩字」……不混淆。把整數7反排名:
i=7: i-1=6; 6 mod 4 = 2 → 數字 2+1 = 3(末位); i = 6 div 4 = 1
i=1: i-1=0; 0 mod 4 = 0 → 數字 0+1 = 1(首位); i = 0 → 停
數字串 = [1, 3] → 字元序號 [0, 2] → 「天玄」
反向驗證:rank(天玄):序號 [0,2] → 數字 [1,3] → 1·4 + 3 = 7。往返一致。而 i=0 唯一對應「空串」。普通進位制下 0/00/000 撞在一起的歧義,在這裡消失——這正是「自由形式」必須用雙射數系的原因。
兩種數系的差別只在邊界(是否需要編碼「長度本身」)。核心永遠是同一句話:詩就是一個數,數就是一首詩。
4. 構件②:受約束子空間的排名/反排名(本技術的皇冠)
§3 枚舉的是「所有字元組合」。但若你要的是格律詩——必須合平仄、必須押韻——那麼合法物件只佔 N^L 裡微不足道的一小塊。難點是:如何對這個「受約束子集」S_valid ⊂ Σ^L 仍然建立 整數 ↔ 物件 的雙射,且不枚舉、不儲存?
這就是排名/反排名的真正用武之地,也是這個技術從「玩具」變「真功夫」的地方。
4.1 約束的形狀
把格律詩的約束抽象成三層結構(這個抽象正是可推廣的關鍵):
- 模板集 Variants:有限個「合法骨架」。律詩只有少數幾個標準平仄式(平起/仄起 × 首句入韻與否)。記為
V = {v_1, …, v_m}。 - 每槽有限域 Domain(slot):在一個給定模板下,第
k個位置的可選字元被約束在某個子集:
- 該位要求「平聲」→ 可選字 =
Ping(平聲字集); - 該位要求「仄聲」→ 可選字 =
Ze(仄聲字集); - 該位是「韻腳」→ 可選字 = 某個韻部
g的成員集。
- 跨槽等價耦合 Coupling:所有韻腳位置必須屬於同一個韻部
g。這是唯一的非局部約束——它把若干槽「綁在一起」。
於是合法空間是一個不交併:
S_valid = ⊔_{v ∈ V} ⊔_{g ∈ Rhymes} Slots(v, g)
Slots(v, g) = ∏_{k} Domain(k; v, g)
在模板 v、韻部 g 下,設平聲自由位 pf 個、仄聲位 z 個、韻腳位 rh 個,則
|Slots(v, g)| = |Ping|^{pf} · |Ze|^{z} · |g|^{rh}
4.2 基數(cardinality)
合法詩總數可閉式算出,不需枚舉:
|S_valid| = Σ_{v ∈ V} ( |Ping|^{pf_v} · |Ze|^{z_v} ) · ( Σ_{g ∈ Rhymes} |g|^{rh_v} )
注意韻腳耦合讓「選哪個韻部」變成一個加權的類別選擇(每個韻部貢獻 |g|^{rh} 條),不是簡單乘法——這正是非局部約束的代價,也是它仍可閉式計數的優雅之處。
4.3 反排名 unrank(整數 → 合法詩)
給定 i ∈ [0, |S_valid|):
1. 選模板 v:依序減去每個模板的容量 count(v)=(|Ping|^pf·|Ze|^z)·Σ_g|g|^rh,
直到 i < count(v)。剩下的 i 就是「在 v 內的局部編號」。
2. 選韻部 g:把 v 內容量再拆成「韻部區塊」。每個 g 的區塊大小
block(g) = |Ping|^pf · |Ze|^z · |g|^rh。依序減去 block(g) 定出 g。
3. 槽內混合基數展開:剩餘編號對「各位置的有限域大小」做一次混合基數
unrank —— 平聲位用基數 |Ping|、仄聲位用 |Ze|、韻腳位用 |g| ——
得到每個位置在其域內的「第幾個」,再查表還原成字。
全程只有大整數的除法與取模,O(L) 次大數運算,不觸碰 |S_valid| 這個天文數字本身。
4.4 排名 rank(合法詩 → 整數)與驗證
反向:給一首詩 c,
1. 驗證 + 認模板:找出 c 符合哪個模板 v 的平仄式;若都不符 → 不合法(回 null)。
2. 認韻部:取所有韻腳位的字,檢查是否同屬一個韻部 g;不一致 → 不合法。
3. 前綴和 + 混合基數 rank:i = (前面所有模板的總容量)
+ (本模板內 g 之前所有韻部的區塊和)
+ (槽內混合基數 rank)。
rank 與 unrank 互逆。「驗證」與「排名」是同一個函數的兩面:能排名 ⟺ 合法。
4.5 雙射的不變式(為什麼成立)
- 前提(韻部劃分,務必明列):假設所有韻部兩兩不相交——每個可入韻的字恰屬一個韻部(真實平水韻即如此)。這是
⊔_g與Σ_g |g|^rh計數成立的必要前提:若兩個韻部共用一字,同一首詩會在兩個(v,g)格各被生成一次,Σ_g|g|^rh就重複計數、雙射破裂(一詩兩號)。(驗算:取重疊群g0={p0,p1}、g1={p1,p2}與 2 個韻腳位,閉式給2²+2²=8但實際只有 7 個相異韻腳組——(p1,p1)被算了兩次。) - 不交性:不同模板的平仄式互斥(平仄序列唯一決定模板,或都不屬於);在上述劃分前提下,同模板的不同韻部也互斥(韻腳的韻部唯一)。故
⊔是真不交併,編號不重不漏。若域允許重疊,則須讓編號攜帶所屬類別(tag)把併集人工不交化,rank/unrank 才仍良定義。 - 完備性:
Slots(v,g)是笛卡兒積,混合基數對笛卡兒積是標準雙射。 - 閉合性:
unrank(i)產出的詩,其平仄與韻必然滿足約束(因為域本身就是約束過的),所以rank(unrank(i)) = i、unrank(rank(c)) = c。
這一節是可被推廣的最值錢的東西:只要你的「合法物件」能寫成
⊔_模板 ⊔_類別 ∏_槽 域
的形狀(模板有限、每槽有限域、跨槽用「同類別」耦合),你就能對它做零枚舉的 整數 ↔ 物件 雙射。格律詩只是這個形狀的一個實例。
4.6 一個經實際運算驗證的受約束範例(Knuth 閘門)
抽象容易自我感覺良好,所以本文把它寫成程式跑過——依 BUG學「修前 fail、修後 pass」的鐵律,論文裡的數字必須是可執行地證明的,不是嘴上說的。
設一個玩具受約束空間(刻意小到能暴力枚舉對帳):
- 平聲字
Ping = {p0,p1,p2}(|Ping|=3)、仄聲字Ze = {z0,z1}(|Ze|=2); - 韻部把平聲字切成兩組:
g0={p0,p1}(大小 2)、g1={p2}(大小 1); - 長度
L=4,兩個模板(位置型別:pf=平聲自由、ze=仄聲、rh=韻腳): v0 = [pf, rh, ze, rh],v1 = [ze, rh, pf, rh](皆pf=1, z=1, rh=2)。
依 §4.2 閉式基數:
count(v) = |Ping|^pf · |Ze|^z · Σ_g |g|^rh = 3^1 · 2^1 · (2^2 + 1^2) = 6 · 5 = 30
|S_valid| = count(v0) + count(v1) = 30 + 30 = 60
實作 §4.3 的 unrank 與 §4.4 的 rank,跑兩道閘門:
| 閘門 | 結果 | |---|---| | 閉式基數 == 暴力枚舉計數 | 60 == 60 ✓ | | 對所有 i ∈ [0,60):rank(unrank(i)) == i | 全通過 ✓ | | unrank 的值域恰好覆蓋合法集(不重不漏) | |image| = 60,與暴力集相等 ✓ |
也順手驗了真實參數的空間量級(N≈2500):五絕 2500^20 ≈ 10^68、七絕 ≈ 10^95、五律 ≈ 10^136、七律 ≈ 10^190——與 §3.1 的表一致。
這一節同時是內容、也是驗證。 它證明 §4 的雙射不是紙上談兵:給編號取詩、給詩取編號,在受約束子空間上嚴格互逆、不重不漏。重建時的第一個里程碑(§10.4 的 M2)就是把這道閘門在你自己的程式裡重現。
5. 構件③:座標 ↔ 索引——確定性可逆綁定
有了 整數 ↔ 物件,還差「整數 ↔ 三維位置」,才能把編號空間變成可飛行的星雲。
5.1 索引 → 位置(scatter)
把編號 i 用一個確定性雜湊打散到三維空間:
pos(i) = ( f(i, 1), f(i, 2), f(i, 3) ) · R
其中 f(i, axis) = hash64(i ⊕ axis) 映到 [-1, 1),R = 雲半徑
hash64 用 splitmix64 風格的位元雪崩(乘以奇常數、右移異或;常見常數如 ⌊2^64/φ⌋)。確定性保證:同一個 i 永遠落在同一點,所以位置不需儲存。
5.2 位置 → 索引(spatial hash)
反過來,當使用者在空間中「飛到某處/點選某處」,要由位置取回該處的詩:
1. 量化座標:q = round(pos · 量化粒度) // 把連續空間切成格
2. 空間雜湊:h = (q.x·P1) ⊕ (q.y·P2) ⊕ (q.z·P3) // P1,P2,P3 為大質數
3. 串流擴展到足夠位元,取模合法空間大小:i = expand(h) mod |S_valid|
其中 P1,P2,P3 是經典的空間雜湊三質數(Teschner 等人提出的 73856093 / 19349663 / 83492791 是這領域的事實標準)。expand 用 splitmix64 連續產生 64-bit 區塊,拼到比 |S_valid| 的位元數還多十幾位,再取模——確保落在合法區間且分布均勻。
注意:scatter(索引→位置)與spatialHash(位置→索引)不是嚴格互逆,而是「一個負責把編號撒進空間、一個負責把空間格子映回編號」。系統靠的不是嚴格可逆,而是兩個方向各自確定性:同編號恆同位置、同格子恆同編號。導航體驗只需要「同一處永遠是同一首詩」,這個性質就夠了。
5.3 確定性 = 零儲存的前提
整個構件③沒有任何狀態。它是純函數。確定性雜湊是「不儲存也能有穩定世界」的全部祕密:世界不是被存下來的,是每次被同一個函數重新算出來的,而純函數每次算的結果一樣,於是看起來像一個固定存在的世界。
6. 構件④:URL 即狀態——內容定址與無後端
把當前選中物件的編號寫進網址查詢字串(如 ?p=<大整數>),並用 history.replaceState 在使用者導航時無刷新同步。載入時讀網址、unrank 回物件、scatter 到位置、把相機飛過去。
原作攜帶的是構件①的「自由形式」編號(§3.2 的babelIndex,over Σ\*),載入時解碼成自由詩;§4 的受約束 rank(lushiIndex)只作顯示用、不進 URL(見 §2 釐清)。重建建議:若要 URL 直接定址到「一首合格律的詩」,可改用 §4 的受約束 rank 當?p=內容——這是相對原作的乾淨改良,代價是 URL 語意從「自由詩座標」變成「合律詩編號」,兩種定址不可混用(同一個數字在兩種解碼下是不同的詩)。
由此同時得到四個性質(下一節證明):
- 零後端:解析網址只需純前端計算,伺服器只送一份靜態檔。
- 可分享:一個 URL 完整重現一首詩與它的位置;複製貼上即傳遞內容本身。
- 天然防爬:HTML 是空殼,內容是前端從編號算的;樸素抓取(含多數「scrape→clone」工具)只得到殼。
- 無限:URL 能放下的數字位數幾乎無上限,於是可定址的內容近乎無限。
可選的第二載體:把同一編號也放進#fragment。#之後不送伺服器、純客戶端路由,適合「分享時也帶狀態、但不污染伺服器日誌」。?與#可並存,互為備援。
7. 系統不變式與性質(為什麼成立)
把四構件合起來,下列性質可由前文直接推出:
| 性質 | 由何保證 | |---|---| | 零儲存(生成層) | 生成的內容 = unrank(i)、位置 = scatter(i),皆純函數、無資料庫。(少數策展真作另從靜態 JSON 載入,不屬生成層。) | | 近乎無限 | 可定址內容量 = |S_valid|,隨 L 指數成長(七律 ~10^190)。 | | 確定性/可重現 | 所有雜湊為純函數;同 i/同位置恆得同結果。 | | 天然防爬 | 內容不在初始 HTML;需執行前端解碼才現形。 | | 可分享 | URL ⊇ 編號(原作為 §3.2 自由形式編號),單一整數重建內容+位置。 | | 合法性內建(受約束路徑) | 走 §4 受約束 unrank(位置→只逛合律詩)時,域已是約束過的子集,產出必合格律;走 URL 自由編號路徑則解碼為自由詩。 |
一個誠實的反面:防爬不是「不可破」。若爬蟲願意執行 JavaScript(如以 headless 瀏覽器渲染——某些 scrape-website 服務的進階模式就會),它仍能抓到算出來的 DOM。真正穩固的不是「防住所有爬蟲」,而是機制本身:內容封進不透明編號、無後端可打、單一 URL 自包含——這讓「複製這個站」退化成「複製一個空殼 + 一個你看不懂的數字」。
8. 與生成範式的接合(從尋找到生成)
paradigms/ 裡反覆出現的主軸——「從尋找到生成」「生成路徑優越性」——在這個技術裡有一個乾淨的具體化:
- 尋找式系統:內容先被生產、儲存、建索引,使用者透過查詢去「找」它。成本在儲存與檢索,上限在「已被存進去的東西」。
- 生成式系統(本技術):內容不被儲存;給定座標,當場生成。上限不是「存了多少」,而是「編號空間有多大」——而編號空間是
N^L,免費的指數。
詩雲做不到《詩雲》小說裡那步「從垃圾裡認出好詩」,因為那一步是價值判斷,不是枚舉。但它把前一半做到了極致:整個生成空間被完整定址、可導航、可分享、零成本。 這恰好印證範式的論點——當「生成」便宜到近乎免費,瓶頸就從「製造/儲存內容」整個轉移到「在無限生成物裡施加意義與選擇」。這個瓶頸轉移本身,就是『其他潛力發展』的入口。
9. 通用化:「X 之雲」抽象介面
詩只是 X 的一個取值。把技術抽乾淨,得到一個介面:任何能實作下列介面的 X,都能變成一座可飛行、URL 即內容、零儲存、近乎無限的「X 之雲」。
9.1 介面定義
interface EnumerableSpace<X> {
cardinality(): BigInt // |S_valid|,閉式可算(不枚舉)
unrank(i: BigInt): X // 整數 → 物件(必落在合法子集)
rank(x: X): BigInt | null // 物件 → 整數(順帶驗證合法性)
render(x: X): Visual // 物件 → 可呈現形態
}
interface Embedding {
scatter(i: BigInt): Vec3 // 索引 → 三維位置(確定性)
locate(p: Vec3): BigInt // 位置 → 索引(空間雜湊 mod |S|)
}
// URL 層、相機/渲染層對所有 X 通用,不必重寫。
只要補上 EnumerableSpace<X> 與一張「字表/原子集」,其餘(座標綁定、URL、3D 導航)幾乎是現成的。
9.2 把 X 換成什麼(實例表)
| X | 原子集 Σ | 約束(模板×域×耦合) | 難度 | |---|---|---|---| | 古典詩(本例) | 字(含平仄、韻部) | 平仄模板 × 平/仄/韻域 × 同韻耦合 | ★★★ | | 旋律 / 和弦進行 | 音符 / 和弦 | 調式音階 × 級數域 × 終止式耦合 | ★★★ | | 合法棋局片段 | 棋步 | 規則合法步 × 局面域 × 將軍/連續性耦合 | ★★★★ | | 形式證明樹 | 推理規則 | 型別/前提模板 × 可用規則域 × 結論一致耦合 | ★★★★★ | | 分子式 | 原子/鍵 | 價數模板 × 成鍵域 × 價數守恆耦合 | ★★★★ | | Shader / 程序紋理 | 運算節點 | 型別模板 × 節點域 × 輸入輸出耦合 | ★★★ | | SVG 紋樣 / 圖騰 | 路徑基元 | 對稱模板 × 基元域 × 閉合/對稱耦合 | ★★ | | 迷宮 | 牆/通道 | 連通模板 × 格域 × 單一連通耦合 | ★★ |
模式都一樣:找到 X 的「合法骨架(模板)× 每槽有限域 × 跨槽等價耦合」結構,§4 的雙射就能套上去。
9.3 難度光譜
- 容易(★–★★):約束是局部的(每槽獨立或只有簡單對稱)。混合基數直接做,幾乎是 §3。
- 中等(★★★):約束是「模板 + 每槽域 + 少量等價耦合」(詩、旋律、紋樣)。正是 §4 的形狀,可閉式計數。
- 困難(★★★★+):約束是全域且互相牽連的(棋局合法性、證明的全域一致性)。此時
S_valid不再是「不交併的笛卡兒積」,閉式計數可能不存在——退路是對更寬鬆的超集枚舉、生成後驗證、用拒絕採樣或更強的組合排名(如對自動機/文法語言的排名)。這條線就通往「對受文法約束的語言做排名」這個更深的計算組合學主題,也是X越往「需要全域語義一致」走、技術越硬的地方。
「其他潛力發展」多半藏在 ★★★★ 那欄:當 X 是有語義、有全域一致性的東西(證明、程式、可運行的設計),這座雲就從「美學玩具」變成「可被導航的解空間」——而導航一個解空間,距離「在解空間裡搜尋/生成有用解」只剩一步。
10. 重建藍圖(clean-room,MSSP / DMS-first)
依房規:先畫、先定憲法、先設計可觀測,再寫碼。以下是可照著建的最小骨架(規模屬 Small:扁平優先、禁止過早分層)。
10.1 FMS(系統憲法・純元資料,不含可執行碼)
- 敘事(為什麼+取捨):要的是「可導航的生成宇宙」。取捨:選擇「每顆星都是真物件、由座標生成」而非「貼圖/預存」——換來無限與防爬,代價是放棄「整體的人為策展」(多數點無意義)。
- 索引(模組+依賴):見 §10.2。
- 註釋(決策+限制):字表必須自建(法律+語義);雜湊必須確定性(零儲存的前提);URL 編號是唯一真值來源(single source of truth)。
10.2 模組分解(SMS 穩定核心 / TMS 可插拔子集)
SMS(穩定核心・介面契約,最小核心原則)
numeral/ 混合基數 + 雙射數系(rank/unrank 基礎) ← 純函數、可單測
space/ EnumerableSpace 介面 + 受約束枚舉骨架 ← §4 的雙射
embed/ scatter / locate(確定性雜湊、空間雜湊) ← §5
urlstate/ rank↔URL、replaceState 同步 ← §6
TMS(可插拔・自包含・可替換;移除測試:拿掉它核心仍可編譯)
lexicon.x/ 具體字表/原子集(換語言、換 X 就換這個)
constraint.x/ 具體約束(格律/調式/規則…)實作 EnumerableSpace
render.x/ 具體呈現(3D 星雲 / 文字卡 / 樂譜…)
DMS(診斷層・要設計進去,不是事後補)
- rank(unrank(i)) === i 的往返自檢(任意抽樣 i)
- locate(scatter(i)) 的穩定性檢查(同點恆同詩)
- |S_valid| 與「實際可達編號上界」一致性檢查
- URL 往返:parse(stringify(state)) === state
移除測試判準:render.x、lexicon.x、constraint.x 拿掉後 numeral/、embed/、urlstate/ 仍能編譯與單測 → 它們是 TMS;反之核心三件是 SMS。
10.3 資料流圖(≤9 元素,A4 放得下)
[URL ?p=i] ──parse──► [BigInt i] ──unrank──► [object x] ──render──► [3D 星 / 詩卡]
▲ │
│ stringify(rank(x)) │ scatter(i)
│ ▼
[history.replaceState] ◄──select──── [camera flyTo pos] ◄──locate──── [使用者點選空間]
兩條路徑、兩個位置來源(重建勿混):上排(URL ⇄ 物件)用構件①的自由rank/unrank,此路徑的位置 =scatter(i)。下排(點選空間「捕詩」)用locate→ §4 受約束unrank取一首合律詩,而這首詩的顯示位置 = 使用者所點的那個三維點本身(不是scatter(其編號))。亦即:已知編號才用scatter定位;當場捕撈則「位置先於編號」。別假設「捕到的詩」會落在scatter(它的編號)——那會讓星星從你點的地方跳走(呼應 §5.2、§13.3)。
10.4 最小可重建核心(MVP)與 Knuth 閘門測試
依 BUG學:每個核心函數先有「修前 fail、修後 pass」的可執行往返測試,再談完成。
MVP 里程碑(每步都有閘門測試才算過)
M1 numeral:unrank/rank 往返(定長 + 雙射數系變長)對隨機 i 全通過。
M2 space :給一個玩具約束(如「4 槽、2 模板、3 韻部」)實作 EnumerableSpace,
cardinality 與「暴力枚舉計數」一致;rank(unrank(i))===i 全通過。
M3 embed :scatter 確定性、locate∘scatter 落在合法區間;同格子恆同編號。
M4 url :parse∘stringify 往返;replaceState 不刷新。
M5 render:先文字版(unrank→印出物件),確認端到端;再接 3D(可借用你現成的星空渲染)。
10.5 與星空渲染的接合
§5 的 scatter(i) 產出的就是一團三維點——這正是你星空 v2 已經會的東西(座標雲 + r3f/three instancing + 站在中心飛行)。把星空的「隨機散佈」換成「scatter(編號) 散佈」,把「點擊辨星卡」換成「locate(位置) → unrank → 詩卡」,**你的星空就直接升級成 X 之雲的渲染層**。換言之 render.x` 你已經寫過大半。
11. 風險、邊界與誠實對照
- 訊號淹沒(最根本的限制):
|S_valid|裡絕大多數是「合格律但無意義」的詩。這座雲解決了「定址與導航」,沒有解決「找到好的」。這正是《詩雲》小說的困境被誠實地保留下來。任何認真的「其他潛力發展」都必須回答:在無限合法物裡,如何施加意義/選擇/評分?(curated 入口、評分函數、人在迴路、或把「選擇」本身也建模成可導航的維度。) - 可發現性:URL 是內容,但「好 URL」本身不可被瀏覽地發現——你要嘛從某處飛到它、要嘛被分享。系統需要「策展入口 / 種子座標 / 評分熱區」來讓人有起點。
- 計數可行性的天花板:§9.3 ★★★★ 那類(全域語義一致的 X)可能沒有閉式
cardinality,雙射退化為「超集枚舉 + 驗證」。要事先誠實標注哪些 X 落在可閉式計數的那一側。 - 技術 vs 財產(界線,接續前面那題):
- 可借鑒(拿走就好):雙射數系、受約束子空間的 rank/unrank、空間雜湊綁座標、URL 即內容——這些是方法/演算法,不受著作權保護。
- 不可照搬:對方的原始碼、字表資料、具體常數選擇之表達、品牌與視覺。重建時這些全部自建。
- 一句話:要的是招,不是他的家具。 招我已經拆給你了;家具我們自己打。
12. 意義工程:在無限中施加選擇(「其他潛力」的技術入口)
§11.1 點出了根本限制:詩雲把「定址與導航」做到極致,卻完全沒碰「從無限合法物裡認出有價值的那些」。這不是缺陷,是分工——它做的是基礎建設。而你說的「其他潛力發展」,幾乎全部落在它之上的這一層:意義工程(meaning engineering)——在一個已被完整定址的無限空間裡施加價值、選擇與用途。 下面是四條可走的路,每一條都以「可導航的雲」為地基。
12.1 把「選擇」變成另一個可導航維度
均勻散佈的雲沒有地形——每顆星一樣亮,眼睛無處落。引入一個評分函數 score: X → ℝ(語意連貫度、美學、或「有用性」),就把均勻雲變成有地形的景觀(fitness landscape):把分數映到亮度/大小/顏色/聚集度,好的東西會自己發光,導航就從「亂飛」變成「沿著梯度爬山」。
代價:10^190 個點不可能全評分。所以只能局部——只對相機鄰域 scatter 出來的點即時評分、即時著色。雲不是被預先點亮的,是隨你飛到哪、哪裡才亮起來。這恰好與構件③的「確定性、按需生成」同構:連「意義」都按需算。
12.2 種子 + 局部搜尋(把生成接回搜尋)
給若干策展種子座標當入口(對應詩雲的 ?a= 詩人模式——已知的好起點)。從種子出發,在 unrank 的鄰域做局部最佳化(梯度、演化、MCTS),找更好的鄰居。
這正是範式「從尋找到生成」之後的下一拍:不是生成 vs 尋找二選一,而是「廉價生成 + 廉價局部搜尋」聯手——生成提供無限且零成本的候選地形,局部搜尋在地形上找峰。生成負責「有什麼」,搜尋負責「往哪走」。
12.3 把約束做得更強 = 把垃圾擋在門外
§4 的約束越強,S_valid 越小、有意義的密度越高。把約束從「合語法」推向「合語義」,等於把更多垃圾排除在可定址空間之外——它們連編號都拿不到。
這條路的盡頭極具威力:當 X = 可驗證的物件(形式證明、可編譯且通過測試的程式、滿足規格的設計),約束 = 「能通過驗證器」,則 unrank 的每一點都是一個合法解。此時這座雲不再是美學玩具,而是解空間的可導航地圖——而「導航一個全是合法解的空間」距離「在解空間裡找有用解」只剩最後一步。(代價見 §9.3:約束強到這種程度,閉式計數通常失效,雙射退化為「對驗證器接受的語言做排名」這個更硬的問題。這也正是 ★★★★ 那一欄、和你 P vs NP 那套思路真正接上的地方。)
12.4 人在迴路 / 集體標註
把人的行為(收藏、分享、停留)當成對無限空間的稀疏標註,反過來訓練 score,於是雲會長出被人關注的熱區。策展從「一次性挑選」變成「持續演化的點亮」。
這直接接上你 paradigms 裡的「集體智能相變」「同步發現的微觀差異」:無限生成空間 + 集體標註 = 一個可被集體認知逐步點亮的宇宙。 一個人點不亮 10^190,但一群人持續地飛、停、收藏,會在無限裡演化出有結構的星座。
12.5 定位
| 層 | 做什麼 | 詩雲 | 「其他潛力」 | |---|---|---|---| | 基礎建設 | 定址、導航、分享、零儲存 | ✅ 已做到 | 當地基 | | 意義工程 | 在已定址的無限裡施加價值/選擇/用途 | ❌ 未碰 | 幾乎全在這 |
詩雲是一座蓋好的無限圖書館——書架完備、座標清晰、任何一本都能瞬間翻到。它沒做的是「告訴你哪本值得讀」。而那,恰恰是把這座圖書館從奇觀變成工具的全部關鍵。
13. 重建陷阱(務必避開)
照房規傳統,把重建時真正會踩的坑先標出來。這些多半是「不踩過不知道」的,列在前面省三輪重啟。
- 變長內容用普通進位制 → 前導零歧義(最致命)。
0、00、000在普通N進位下同值,於是「字串 ↔ 整數」不再是雙射,空串無法唯一編碼、不同長度會撞號。變長一律用雙射數系(數字1…b)。定長才可以用普通混合基數。先想清楚你的內容是定長還是變長,再選數系。
- 用
Number而非BigInt→ 在2^53悄悄爆掉。編號動輒近百位,遠超 IEEE-754 雙精度的安全整數上限(2^53−1)。所有 rank/unrank/雜湊的整數運算必須全程BigInt。一個不小心混入的Number轉換,會在某個臨界值之後產出「看起來對、其實錯」的編號——而且很難察覺,因為小編號都正常。這正是 BUG學「注入↔顯現時間距離 ∝ 修復成本」的典型:錯在編碼、爆在某個大數,中間隔很遠。
- 以為
locate(scatter(i)) == i,或以為「捕到的詩」住在scatter(其編號)。§5.3 已說:散佈與空間雜湊不是嚴格互逆。更具體地,有兩條產生位置的路徑:(a) 已知編號(如還原分享 URL)→ 位置 =scatter(i);(b) 點虛空當場捕詩 → 位置 = 使用者所點的那個點,編號由locate反求。別把兩者混為一談、別假設往返恆等;系統只依賴「同編號恆同位置、同格子恆同編號」這兩個各自的確定性。把它當互逆,會在「點選 → 取詩 → 再定位」處讓星星跳走。
- 字表是凍結的協定,不是可調的參數。字表的內容與順序一旦對外發布,就不能再改——改一個字的序號,所有舊 URL 指向的詩全部變了。字表必須像資料庫 schema 一樣版本化、append-only(要擴充只能在尾端加字,不能重排)。把「字表凍結」寫進 FMS 的限制欄。
- 確定性雜湊的跨平台一致性。
scatter/locate要在不同瀏覽器、不同裝置、甚至不同語言的後端重算出同一個結果,否則「分享的 URL 在你那裡是另一首詩」。務必:全部運算mod 2^64(明確 wrap)、用同一組常數、避免任何浮點參與雜湊主路徑(浮點的捨入跨平台不保證一致)。座標量化那一步尤其要定死粒度與捨入規則。
- 約束強到沒有閉式
cardinality還硬算。§9.3 ★★★★ 那類(全域語義一致的X)的合法集不是「不交併的笛卡兒積」,|S_valid|可能根本沒有閉式。硬套 §4.2 會算出錯的基數,連帶unrank的取模範圍全錯。事先判斷你的X落在難度光譜哪一格:可閉式計數的(★★★)才直接套 §4;否則改走「超集枚舉 + 驗證 + 拒絕採樣」或語言排名。
- URL 長度上限與分享媒介截斷。近百位數字還好,但若
X更大(長文、複雜結構),編號可能長到撞上瀏覽器 URL 長度上限或被聊天軟體/QR 截斷。對策:可在編號上加可逆壓縮、或改用較高基數(Base62/Base64url)縮短字面長度——但壓縮層要與語意層解耦(壓縮只動表示,不動「編號 ↔ 物件」的雙射)。
scatter分布不均 → 視覺結塊。若散佈雜湊品質不夠,星會在某些區域堆積、某些區域空洞,破壞「無垠星海」的錯覺。用品質好的位元雪崩(splitmix64 風格),並對三個軸用不同的鹽(如i ⊕ axis),避免三軸相關產生對角條紋。這與星空專案「球面取點別用random*π」是同一類陷阱:取樣分布的細節決定觀感。
- 把渲染和枚舉耦死。常見誘惑是「邊飛邊直接從座標算詩、塞進渲染迴圈」。但
unrank+ 約束驗證可能不便宜,塞進每幀會掉格。渲染層只認座標與少量已解出的物件;解碼放在選取/鄰域預取時做,並快取。保持 §10.2 的 SMS/TMS 邊界——render.x不該知道constraint.x的內部。
- 忘了「無限 ≠ 有意義」就對外宣稱。展示時容易把「能定址
10^190首詩」講成「有10^190首好詩」。誠實點:絕大多數是合格律的噪音(§11.1)。把這點寫進文案,否則使用者飛兩下發現全是亂句,會覺得被騙——而技術本身明明很誠實。
一句話:雙射要用對數系、整數要用 BigInt、字表要凍結、雜湊要跨平台一致、別把「無限」當「有意義」。 這五條踩過任何一條,都會在很後面才爆。
14. 結語
這個網頁的「強」,不在它是一個關於詩的作品,而在它不小心示範了一個通用技術:當內容能由座標確定性地生成,內容就不必被儲存、不必被尋找、可以被當成空間來飛行、可以被一個整數完整分享。它是「從尋找到生成」這條範式線上一個能用滑鼠拖動的具體證據。
原作者把它當「有趣的網頁比賽」。但他手上的,是「生成式內容宇宙」的一個可運行原型——而這類宇宙真正的潛力,在他畫的這條線之外:把 X 從「詩」換成有語義、有用、有全域一致性的東西時,這座可導航的雲就從美學玩具,變成一個可被飛行的解空間。
下一步——現場重建——就照 §10 的 MSSP 骨架走:先把 numeral/ 與一個玩具 constraint/ 的往返閘門測試做出來(M1–M2),其餘接上你現成的星空渲染。招在手上了,剩下的是把它打成你自己的。
寫實與寫意是同一顆球的內外兩面;儲存與生成,是同一個空間的兩種看法。它選了生成——於是一張紙都不用,就裝下了比宇宙原子還多的詩。