Posts [posts]

從有限元素中隨機取未使用的元素 [tr-D6VS]

元素集合可以轉換為正整數集合,因此問題可以寫成:給定範圍 [0,N)[0, N),已使用之數字集合為 UU,最簡單的演算法就是排除 UU

[0,N)∖U[0, N) \setminus U

從中隨機取一個元素

問題 [local-0]

然而排除已使用元素是固定成本,在已使用元素會變多的情況下更是越來越大的成本。隨機抽一個,再查看有沒有撞上已經使用的位置反而更有效。但這樣做的界線到底在哪裡?

幾何分布 [local-1]

對於空間大小 NN 與已使用的 UU,隨機抽一個位址命中未使用元素的機率是

p=N−∣U∣Np = \frac{N - |U|}{N}

假設每次抽樣互相獨立,那「抽到第 kk 次才第一次成功」服從幾何分布

(1−p)k−1p(1-p)^{k-1} p

連續 kk 次都失敗(選到已使用的位置)的機率是 (1−p)k(1 - p)^k,期望次數是

1/p=NN−∣U∣1/p = \frac{N}{N - |U|}

所以我們可以看到 ∣U∣|U| 只佔 NN 一小部分時,期望次數幾乎是常數,∣U∣=N/2|U| = N/2 時也只是2次,跟排除法的 O(N)O(N) 差距明顯。要等到 N−∣U∣N - |U| 幾乎為 00 時,這才開始稱得上成本

計算重試上限 [local-2]

(1−p)k(1 - p)^k 隨 kk 遞減趨近 00,但對任何有限的 kk 都不等於 00,也就是不存在有限次數可以保證我們抽到未使用的數字。所以這裡要找的不是「幾次一定會中」,而是「願意接受多大的失敗機率」

給定可以接受的失敗機率 ε\varepsilontr-notes 取 10−910^{-9},我們想找出滿足 (1−p)k≤ε(1 - p)^k \le \varepsilon 的最小 kk

先對兩邊取 ln⁡\ln,ln⁡\ln 單調遞增所以不等號方向不變

ln⁡((1−p)k)≤ln⁡ε\ln((1 - p)^k) \le \ln \varepsilon

根據Logarithm power rule可以得出

kln⁡(1−p)≤ln⁡εk \ln(1 - p) \le \ln \varepsilon

p∈(0,1)p \in (0, 1) 時 ln⁡(1−p)<0\ln(1 - p) < 0,所以同除要翻轉不等號

k≥ln⁡εln⁡(1−p)=ln⁡(1/ε)ln⁡(1/(1−p))k \ge \frac{\ln \varepsilon}{\ln(1 - p)} = \frac{\ln(1/\varepsilon)}{\ln(1/(1 - p))}

第二個等號來自分子分母同乘 −1-1,讓兩邊都寫成正數。取ceiling就是可拿來做嘗試次數的最小 kk

但計算 ln⁡(1/(1−p))\ln(1/(1 - p)) 太麻煩了,所以實作上用 m/pm/p 當上限這裡 m=ln⁡(1/ε)m = \ln(1/\varepsilon),而 1/p1/p 就是上面的期望次數。這個做法是因為如果把分母展開成無限級數注意收斂條件

ln⁡(11−p)=∑n=1∞pnn=p+p22+p33+⋯\ln(\frac{1}{1 - p}) = \sum_{n = 1}^\infty \frac{p^n}{n} = p + \frac{p^2}{2} + \frac{p^3}{3} + \cdots

p∈(0,1)p \in (0, 1) 時每一項都是正的,因此可以看出 ln⁡(1/(1−p))≥p\ln(1/(1 - p)) \ge p被砍掉的高次項正好說明了高估多少

分母變大則整體變小,所以 m/ln⁡(1/(1−p))≤m/pm / \ln(1/(1 - p)) \le m/p,也就是 m/pm/p 大於等於 kk 總是成立。因此這是安全的高估,代價是最壞情況多重試幾次,但讓我們不必計算 ln⁡(1−p)\ln(1 - p)在 p→1p \to 1 時數值上也不穩,這是另一個不採用的理由

最後這個上限還要跟 NN 取 min。期望次數 1/p=N/(N−∣U∣)1/p = N/(N - |U|) 本身其實永遠不會超過 NN(最壞的 N−∣U∣=1N - |U| = 1 時剛好等於),但 m/pm/p 可能會超過:N−∣U∣<m≈20.7N - |U| < m \approx 20.7 時就有 m/p>Nm/p > N。既然這時候成本已經比掃過整個空間還高,應該放棄然後退回過濾再抽取的演算法

結論 [local-3]

我們分析了抽取的上限是怎麼推導出來的,並且計算超過上限就退回一開始的排除法。所以最壞情況是 O(N)O(N) 而不是不會停;另外空間真的滿了會直接報錯,這也是為什麼上面的分析不涉及 p=0p = 0

這個演算法用在 raco tr next 的 --random 功能上

部落格作為一個fediverse帳號 [8664]

延續部落格問題挑戰的規劃,我研究了怎麼讓blog作為fediverse帳號存在。我希望部落格文章會得到一個fediverse post,有人在fediverse上跟post互動時,可以在部落格本體看到變化。這樣就等於借用了fediverse作為留言系統

這個專案公開端點是 https://fedi.dannypsnl.me,借用大量的Cloudflare(CF)功能與Fedify完成:

  • D1:跟隨者、貼文、互動
  • KV:Fedify的快取
  • Queue:投遞跟收信
  • R2:貼文附圖的儲存

ActivityPub需要什麼? [local-0]

一個fediverse帳號是一個HTTP服務,要能回答這幾件事:

  • /@blog:actor,帳號本身
  • /@blog/inbox:別人寄東西來的地方
  • /@blog/outbox、followers、following:這個帳號的公開集合
  • /.well-known/webfinger:把 @blog@fedi.dannypsnl.me 這個名字換成上面那個actor的網址

這些都能用Fedify處理,因為要簽HTTP Signatures,這塊很麻煩所以要盡量復用被驗證過的程式。Actor的宣告:

builder.setActorDispatcher("/@{identifier}", async (ctx, identifier) => {
  if (identifeir !== "...") return null;
  const keys = await ctx.getActorKeyPairs(identifier);
  return new Person({
    // ...
    publicKey: keys[0].cryptographicKey,
    assertionMethods: keys.map((k) => k.multikey),
  });
})

Actor的id是永久的 [local-1]

我一開始把Actor放在 /users/blog,後來改成 /@blog。這樣會非常麻煩,因為那個id改掉之後已經追蹤的帳號手上那個id就指向一個404……

補救就要用ActivityPub自己的move功能。舊網址得回一個退休的Actor,用 successor 指向新的;新的Actor用 aliases 認領舊的,兩邊對得起來遠端才會相信。然後從舊id發一個 Move 給所有跟隨者,而且要用舊id的key id去簽

RSS就是資料來源 [local-2]

利用RSS的話,就不需要改任何現有程式。Worker用cron抓取

[triggers]
crons = ["0 0 * * *"]

item的 guid 作為識別機制:D1裡貼文的主鍵是它,Note的網址是 /@blog/notes/<addr>,前端要問互動的時候拿 location.pathname

同步功能先建立草稿,不會馬上發佈。因為第一次同步會讀到整個feed,直接發就是把站上所有文章一次丟到別人的timeline上。所以另外建立了一個用CF存取規則保護的admin頁面管理這些草稿

用Queue投遞,這裡有個坑,Queue裡的訊息是包裝過的:

const result = await workersQueue.processMessage(message.body);
if (!result.shouldProcess) { message.retry(); continue; }
await federation.processQueuedTask(env, result.message);

我原本直接把 message.body 丟給 processQueuedTask,它就永遠對不上任何一種task,然後安靜地什麼都不做

新follower的回填 [local-3]

Mastodon不會自己爬你的outbox,它只顯示追蹤之後推給它的活動。所以剛追蹤的人看到的是一個空帳號。因此Follow進來時除了回Accept,還要把已經發過的文章補送給這個人:

const posts = await listPosts(ctx.data, "published");
for (const post of posts) {
  await ctx.sendActivity({ identifier: IDENTIFIER }, follower,
                         buildCreateNote(ctx, IDENTIFIER, post.addr));
}

重送本身是安全的,活動的id是從addr算出來的(/@blog/notes/<addr>#create),fediverse站點看到收過的id就不會再存一次

互動 [local-4]

inbox要處理的東西其實不多

  • Create 帶一個Note:回覆
  • Like:讚
  • Announce:轉發
  • Delete、Undo:把上面存進去的刪掉

回覆只認 inReplyTo 指向我自己的Note的那些,所以一整串討論只會留下直接的reply,而不是整個討論串。那些內容屬於原本的伺服器,我這邊只知道有人對這篇文章說了什麼,剩下的點連結過去看

回覆的可見性是回覆者自己選的,跟被回覆的文章無關。私訊回覆一樣會進inbox,因為Mastodon一定會把被回覆的人列進收件人。所以存的時候要先分類:

function classifyVisibility(toIds, ccIds) {
  const pub = PUBLIC_COLLECTION.href;
  if (toIds.some((u) => u.href === pub)) return "public";
  if (ccIds.some((u) => u.href === pub)) return "unlisted";
  return "private";
}

private訊息在公開端點不應該列出

把互動放到文章裡面 [local-5]

有一個公開端點 GET /interactions/:addr 可以看到這篇文章的互動,所以blog這邊用一支 interactions.js 根據 location.pathname 作為addr去問,有互動資料才顯示fediverse區塊參見部落格問題挑戰2026-08-18 · Lîm Tsú-thuàn

不受信任的HTML不應該進 innerHTML,所以先解析成一個不會執行的document,只把文字讀回來

最後是CORS。因為這個端點不吃憑證、也只回公開的資料,所以worker那邊的端點填 *

Access-Control-Allow-Origin: *

部落格問題挑戰 [MNIW]

看ltlnx的部落格時看到這個挑戰,剛好也想寫點什麼所以就來寫吧!

  1. 你當初為什麼開始寫部落格?
  2. 你使用什麼平台來管理你的部落格?為什麼選擇它?
  3. 你之前有在其他平台上寫過部落格文章嗎?
  4. 你如何撰寫文章?例如,使用本地編輯工具,還是在部落格的後台/控制面板中編寫?
  5. 你什麼時候最有寫作靈感?
  6. 你會在寫完後立即發布,還是會先存成草稿醞釀一下?
  7. 你部落格上最喜歡的文章是哪一篇?
  8. 你對部落格有什麼未來計畫嗎?例如重新設計、搬到另一個平台,或是加入新功能?

你當初為什麼開始寫部落格? [local-0]

當初只是想紀錄一些學習上的問題,因為那時候看到「學東西最好的方式就是教別人」,所以就順便把筆記變成部落格

你使用什麼平台來管理你的部落格?為什麼選擇它? [local-1]

我用自己開發的靜態網站產生器tr-notes,工具的網站 https://tr-notes.srht.site/

tr-notes的起源是我使用forester的時候一直遇到breaking changes,但大抵上來說我覺得這種軟體很好用(可以讀 https://www.forester-notes.org/QHXS/index.xml 了解一下forester的一些設計理念)。所以我就開始思考怎麼用racket實現這些效果,主要技術是scribble的Text Generation跟dynamic-rerequire

就理念上來說,我用forest風格的專案目的是

  • transclusion功能
  • 不用特別幫檔案想名稱常青筆記的困難2025-06-10 · Lîm Tsú-thuàn 簡言之就是本體論在長期不具有可維護性,生成一個36進位的4碼數字作為位址
  • 良好的數學渲染支援,包含tikz支援
  • 雙向連結

另外就是tr-notes可以引入racket程式碼編寫專用功能:

你之前有在其他平台上寫過部落格文章嗎? [local-2]

你如何撰寫文章?例如,使用本地編輯工具,還是在部落格的後台/控制面板中編寫? [local-3]

通常是VSCode,因為我也寫了編輯extension

  • 可以即時看到tr-notes渲染結果

  • 可以用 Meta + K 搜尋卡片

  • 可以用 Meta + Shift + M 插入卡片提及語法

你什麼時候最有寫作靈感? [local-4]

上班的時候。主要是通過翻過去的筆記跟目前遇到的問題(來源不定)

你會在寫完後立即發布,還是會先存成草稿醞釀一下? [local-5]

會有很多草稿,但草稿真的很難完成

你部落格上最喜歡的文章是哪一篇? [local-6]

你對部落格有什麼未來計畫嗎?例如重新設計、搬到另一個平台,或是加入新功能? [local-7]

我一直有希望加入 https://webmention.io/,但這協定會因為各種原因失效(包含但不限於cloudflare阻擋機器人讀取email),可能會看有沒有其他替代方案

跟Fediverse整合?但我還沒有研究fediverse協議的狀態

因為想要TeXmacs的所見即所得編輯功能,可能會開發一個tr-notes用的或是通用編輯器?但這畢竟也蠻麻煩的,我也不確定會不會真的動工xd

To CPS or not to CPS:編譯理論之爭 [ASEH]

在什麼是CPS?我們討論過CPS是怎樣的程式風格,以及編譯器可以怎麼利用這種風格。但我們該不該使用CPS呢?這曾經引發一系列的爭論

編譯理論之爭 [local-0]

我個人的觀點比較接近 [Whatever2019],但傾向採用delimited continuation(並整合runtime支援),局部控制流的編譯使用direct style。這就引出了Bowman的A Low-Level Look at A-Normal Form,這篇論文主張「The traditional view of ANF that normalizing commuting conversions is hard, found in formal models and informed by high-level calculi, is wrong」。Bowman認為可以發展出一個normal form具有以下性質

  • 容易normalizing commuting conversion
  • 不需要join points
  • 不需要code duplication
  • 不需要在inline之後renormalization
  • 容易擴展出新的lexically scoped effects

並且Bowman確立了ANF跟monadic form的關聯與intensional properties(分析abstract machine),引導出了imperative monadic form這個形式,設計出untyped lambda-calculus (with scoped regions) 可用的compiler pipeline。最後證明這樣的compiler在stack跟memory行為上都比ANF有所改善

The main take-away from this work is that, in general, monadic form should be preferred over ANF, and A-normalization should only be done in a low-level imperative intermediate form. This maximizes the advantages of each form, and avoids all the standard problems with ANF.

Definition A-normal form (ANF) [1RPZ]

ANF常被叫做administrative normal form,好像是個模糊的「把administrative redex消掉」的說法。但最好想成以下的定義:對給定的A-reductions集合,化約會得到的normal form

Lambda calculus的A-normalization可以寫成以下定義

v::=ι∣x∣(λ (x) e)e::=v∣(op e⃗)∣(e e)∣(let (x e) e)∣(if0 e e e)E::=⋅∣(let (x E) e)∣(if0 E e e)∣(E e)∣(v E)∣(op v⃗ E e⃗)O::=v∣op\begin{aligned} v &::= \iota \mid x \mid (\lambda\ (x)\ e) \\ e &::= v \mid (op\ \vec{e}) \mid (e\ e) \mid (\text{let}\ (x\ e)\ e) \mid (\text{if0}\ e\ e\ e) \\ E &::= \cdot \mid (\text{let}\ (x\ E)\ e) \mid (\text{if0}\ E\ e\ e) \mid (E\ e) \mid (v\ E) \mid (op\ \vec{v}\ E\ \vec{e}) \\ O &::= v \mid op \end{aligned}

A-reductions

E[(let (x e1) e2)]→A(let (x e1) E[e2])A1where E≠⋅E[(if0 v e1 e2)]→A(if0 v E[e1] E[e2])A2where E≠⋅E[(O v⃗)]→A(let (x′ (O v⃗)) E[x′])A3where E≠⋅,E≠E′[(let (x ⋅) e)],fresh x′\begin{aligned} E[(\text{let}\ (x\ e_1)\ e_2)]\quad\rightarrow_A\quad &(\text{let}\ (x\ e_1)\ E[e_2]) \quad &A_1 \\ &\text{where}\ E \ne \cdot \\ E[(\text{if0}\ v\ e_1\ e_2)]\quad\rightarrow_A\quad &(\text{if0}\ v\ E[e_1]\ E[e_2]) \quad &A_2 \\ &\text{where}\ E \ne \cdot \\ E[(O\ \vec{v})]\quad\rightarrow_A\quad &(\text{let}\ (x'\ (O\ \vec{v}))\ E[x']) \quad &A_3 \\ &\text{where}\ E \ne \cdot, E \ne E'[(\text{let}\ (x\ \cdot)\ e)], \text{fresh}\ x' \end{aligned}

化約成的A-normal form(也就是說,不斷套用A-reductions應該讓程式變成以下形式,這些形式套用A-reductions不會再化簡)

(Values)V::=ι∣x∣(λ (x) M)(Computations)N::=V∣(V V)∣(op V⃗)(Configuration)M::=N∣(let (x N) M)∣(if0 V M M)\begin{aligned} (Values)&\quad &V &::= \iota \mid x \mid (\lambda\ (x)\ M) \\ (Computations)&\quad &N &::= V \mid (V\ V) \mid (op\ \vec{V}) \\ (Configuration)&\quad &M &::= N \mid (\text{let}\ (x\ N)\ M) \mid (\text{if0}\ V\ M\ M) \end{aligned}

我們通常會假設綁定中涉及的變數唯一且考慮 α\alpha-equivalence

ANF的缺點 [RG5R]

ANF可以保證每個計算都由一系列的let序列化,並且所有operands都一定是values,從而對指令語言後端編譯是好用的表示形式。但ANF也有一些缺點

ANF在 β\beta-reduction 下不封閉 [local-0]

ANF的缺點之一是在 β\beta-reduction 下不封閉,比如

(let (x ((λ (y) M) V)) x)=(let (x M[y:=V]) x)(\text{let}\ (x\ ((\lambda\ (y)\ M)\ V))\ x) = (\text{let}\ (x\ M[y:=V])\ x)

但這個表達式不是合法的ANF,在RHS位置 M[y:=V]M[y:=V] 不被允許出現。這讓ANF β\beta-equivalence必須重新normalize所有commuting conversions

這對最佳化來說是一個缺陷,因為 β\beta-equivalence model了inlining optimizations;而renormalize是不便又昂貴的計算

A2A_2 會導致程式碼指數級的增長 [local-1]

我們可以觀察以下案例套用 A2A_2 的結果

let x := if0 (if0 (if0 0 0 1) 0 1) 0 1
in LARGE

因為continuation E1[♢]E_1[♢] 是 let x := ♢ in LARGE,所以第一次轉換得到

if0 (if0 (if0 0 0 1) 0 1)
  E1[0]
  E1[1]

再來 E2[♢]E_2[♢] 是 if0 ♢ E1[0] E1[1],好吧,那就得到

if0 (if0 0 0 1)
  E2[0]
  E2[1]

再來 E3[♢]E_3[♢] 是 if0 ♢ E2[0] E2[1],所以是

if0 0
  E3[0]
  E3[1]

最後我們攤開來看就得到

if0 0
  if0 0
    if0 0
      let x := 0 in LARGE
      let x := 1 in LARGE
    if0 1
      let x := 0 in LARGE
      let x := 1 in LARGE
  if0 1
    if0 0
      let x := 0 in LARGE
      let x := 1 in LARGE
    if0 1
      let x := 0 in LARGE
      let x := 1 in LARGE

所以3層if的堆疊得到8個 LARGE

到這裡,Bowman就問說,如果ANF有這麼多問題,那麼與其去修復它,為什麼不要禁止 A2A_2 轉換然後允許 let x := if0 V M1 M2 in M 這個形式呢?畢竟這個形式說穿了也就是

begin:
  if0 V
    set! x M1
    set! x M2
  M

這完全是指令語言後端可以接受的程式!事實上,這早就已經是實用編譯器的常見做法

A-normalization對非monadic effect不安全 [local-2]

Bowman舉了letregion作為案例,並認為沒有文獻討論過這個缺陷。注意到Bowman不想重複join point的老路,也就是要避免用local continuation處理這個問題

Join Points [Z97H]

對於A2A_2 規則引入的程式碼爆炸,除了直接改成用CPS,傳統方式是引入一個local continuation來處理,這種continuation常叫做join point

let x := if0 (if0 (if0 0 0 1) 0 1) 0 1
in LARGE

我們轉換成

let j y := LARGE
if0 (if0 (if0 0 0 1) 0 1)
  let x := 0 in j x
  let x := 1 in j x

然後以此類推,我們就不會產生重複的程式碼。然而join points也有很明顯的缺點,比如引入更多的allocation(lambda的runtime表示closure並不是免費的),並且阻止對 jj 的最佳化,這往往導致實務編譯器必須考慮引入相應的join points algebra去處理一系列的轉換問題

那除了CPS跟join points我們還可以怎麼辦?Bowman考慮了另一個形式monadic form,並正式處理monadic form跟ANF之間的關聯

Monadic form [UP3Y]

如果讓let表示monadic bind,讓return隱含在values下,那monadic form有以下commuting conversions

let (x U) E[x]=E[U](Left Identity)let (x C) x=C(Right Identity)let (y (let (x C) C1)) C2=let (x C) (let (y C1) C2)(Associativity)let (x (if0 U C1 C2)) C=if0 U (let (x C1) C) (let (x C2) C)(Commute)\begin{aligned} \text{let}\ (x\ U)\ E[x] &= E[U] \quad&\text{(Left Identity)} \\ \text{let}\ (x\ C)\ x &= C \quad&\text{(Right Identity)} \\ \text{let}\ (y\ (\text{let}\ (x\ C)\ C_1))\ C_2 &= \text{let}\ (x\ C)\ (\text{let}\ (y\ C_1)\ C_2) \quad&\text{(Associativity)} \\ \text{let}\ (x\ (\text{if0}\ U\ C_1\ C_2))\ C &= \text{if0}\ U\ (\text{let}\ (x\ C_1)\ C)\ (\text{let}\ (x\ C_2)\ C) \quad&\text{(Commute)} \end{aligned}

ANF可以定義成monadic form用associativity跟commute這兩條規則normalize的結果

Monadic form的缺點是沒那麼正規化,會有更多等價但不同的表達形式。有辦法同時得到A的正規化跟避免程式膨脹嗎?這就是non-strict monadic form的用途

Definition Non-strict monadic evaluation contexts與B-normalization [PIBK]

Non-strict monadic evaluation contexts EbE^b

Eb::=⋅∣(if0 Eb e e)∣(Eb e)∣(v Eb)∣(op v⃗ Eb e⃗)E^b ::= \cdot \mid (\text{if0}\ E^b\ e\ e) \mid (E^b\ e) \mid (v\ E^b) \mid (op\ \vec{v}\ E^b\ \vec{e})

B-reductions

Eb[(let (x e1) e2)]→let (x e1) Eb[e2]B1where Eb≠⋅Eb[(if0 v e1 e2)]→let (y (if0 v e1 e2))Eb[y]B2where Eb≠⋅,fresh yEb[(O v⃗)]→let (y (O v⃗)) Eb[y]B3where Eb≠⋅,fresh y\begin{aligned} E^b[(\text{let}\ (x\ e_1)\ e_2)] \quad\rightarrow\quad &\text{let}\ (x\ e_1)\ E^b[e_2]\quad &B_1 \\ &\text{where}\ E^b \ne \cdot \\ E^b[(\text{if0}\ v\ e_1\ e_2)] \quad\rightarrow\quad &\text{let}\ (y\ (\text{if0}\ v\ e_1\ e_2)) E^b[y]\quad &B_2 \\ &\text{where}\ E^b \ne \cdot, \text{fresh}\ y \\ E^b[(O\ \vec{v})] \quad\rightarrow\quad &\text{let}\ (y\ (O\ \vec{v}))\ E^b[y]\quad &B_3 \\ &\text{where}\ E^b \ne \cdot, \text{fresh}\ y \end{aligned}

如果定義 B={B1,B2,B3}B = \{B_1, B_2, B_3\},那麼這組規則會確保evaluation position上的non-value會被換成value。

Example 避免程式指數爆炸成長 [local-0]

相比起 A2A_2 規則,B2B_2 專門用來避免程式指數爆炸的轉換。我們再次回到失敗案例然後推導變換過程

let x := if0 (if0 (if0 0 0 1) 0 1) 0 1
in LARGE

我們改用 Eb[−]E^b[-] 來轉換,由於不抽取let-if,所以我們要直接處理rhs,於是

let x :=
  let y := if0 (if0 0 0 1) 0 1
  in if0 y 0 1
in LARGE

下一步

let x :=
  let z := if0 0 0 1
  in let y := if0 z 0 1
  in if0 y 0 1
in LARGE

所以這確實不會導致大量的程式碼重複

最後我們有辦法同時得到A跟B化簡的好處嗎?可以的,這就是最終形式imperative monadic form

Imperative monadic form [YI0W]

藉由分析ANF跟monadic form的機器表示的意圖,Bowman提出了Imperative monadic form。只要我們讓

let x := a
in b

等於

set! x := a
b

的別稱。在語意上commuting conversion就不再是困擾了

set! y := {
  set! x := t
  t
}
t

等於

set! x := t
set! y := t
t

箝套的if也不再是問題

set! y := if v t1 t2
t

等於

if v
  set! y := t1
  set! y := t2
t

注意到右邊幾乎已經是指令語言可以處理的形式了

Definition Syntax [local-0]

注意到為了支援對if expression的轉換,需要加入if statement

t::=(begin s⃗ t)∣v∣(if0 v t t)∣(call v v)∣(op v⃗)s::=(begin s⃗)∣(set! x v)∣(if0 v s s)∣(set! x t)∣(set! x (op v⃗))∣(set! x (call v v))v::=(λ (x) t)∣ι\begin{aligned} t &::= (\text{begin}\ \vec{s}\ t) \mid v \mid (\text{if0}\ v\ t\ t) \mid (\text{call}\ v\ v) \mid (op\ \vec{v}) \\ s &::= (\text{begin}\ \vec{s}) \mid (\text{set!}\ x\ v) \mid (\text{if0}\ v\ s\ s) \mid (\text{set!}\ x\ t) \mid (\text{set!}\ x\ (op\ \vec{v})) \mid (\text{set!}\ x\ (\text{call}\ v\ v)) \\ v &::= (\lambda\ (x)\ t) \mid \iota \end{aligned}

AB-normalization [UQJG]

根據Imperative monadic form我們已經可以定義AB-reduction

(set! x (if0 v t1 t2))→AB(if0 v (set! x t1) (set! x t2))AB1(set! x (begin s⃗ t))→AB(begin s⃗ (set! x t))AB2\begin{aligned} (\text{set!}\ x\ (\text{if0}\ v\ t_1\ t_2)) \quad&\rightarrow_{AB}\quad (\text{if0}\ v\ (\text{set!}\ x\ t_1)\ (\text{set!}\ x\ t_2)) \quad &AB_1 \\ (\text{set!}\ x\ (\text{begin}\ \vec{s}\ t)) \quad&\rightarrow_{AB}\quad (\text{begin}\ \vec{s}\ (\text{set!}\ x\ t)) \quad &AB_2 \end{aligned}

這種方案避開了join points、編譯器不需要引入整套CPS風格、也不會造成程式碼膨脹(就像B-normalization),對stack使用最佳化(就像A-normalization)。這個做法的關鍵想法在于lexical expressions一定要在A-normalize之前先轉換成循序述句表達

AB-normalization還可以根據新的effect調整,像Bowman就展示了letregion的案例

跟CPS不一樣的是,AB-normalization解決的是local control flow的commuting conversion問題,如果目的是處理non-local control flow的編譯,那CPS可能還是有用途,只是現代的編譯器更可能直接在runtime端實作需要的continuation系統,減少轉換的工作

最終pipeline [local-1]

lambda-calculus
  --> monadic form            (只有 sequencing,不碰 commuting conversion)
  --> imperative monadic form (lexical binding 降成 set!)
  --> AB-normal form          (在 statement 上做 A-normalization)

對照傳統的

lambda-calculus
  --> ANF             (sequencing + commuting conversion 一起做)
  --> code gen

傳統方法的A-normalization compiler必須寫成CPS風格(compiler自己拿一個meta-level的continuation κ\kappa 來累積上下文),還要在target language裡處理join point的表示;而monadic版本的compiler就只是遞迴地sequence,每個子項回傳的都是可以互相組合的

沒解決的:case-of-case [local-2]

這個方法有一個缺點:我們不確定AB-normal compiler能不能處理case-of-case

(if0 (if0 e 1 0) 5 6)

ANF compiler會產生 (if0 e (if0 1 5 6) (if0 0 5 6))——分支被複製了,但接著partial evaluation一步就化簡成 (if0 e 6 5)。AB-normal compiler生出的是 (let (x (if0 e 1 0)) (if0 x 5 6)),沒有複製程式碼,但也就沒辦法做同樣的化簡

Maurer等人的join point calculus 處理這個例子處理得很好,代價是引入join point。Bowman的提議是把條件位置分離成一個boolean的sublanguage,再讓一個簡單的boolean optimizer去化簡,這是Chez的做法,表示這可能這是實務上有效的方案

§6.3談到AB normalizing也可以用monadic language配合state monad表示,不見得要做成imperative language。但這種做法用在處理多個不同effect上會導致著名的monad不可組合問題,所以就算要這麼做,可能也要考慮用單一大monad包含所有此階段需要的effect

這不是新發明,只是沒有命名 [local-3]

論文最後把幾個高效能編譯器抓出來對照,說它們早就在做AB-normalization了

  • Chez Scheme:L10轉換直接把let/let跟let/if的commuting conversions轉換成imperative IR
  • TIL(ML編譯器)的B-form是一個monadic form IR
  • SIL(ML to Ada compiler)

用論文自己的話說就是:這些編譯器早在這個方法被辨識出來之前就使用它了

色彩繽紛的範疇論 [Z00L]

我在這裡想要用範疇論(category theory,又稱抽象廢話)展示跟大家印象中不一樣的數學,有興趣深入學習的讀者可以在最後找到進一步的學習資源。範疇論是從代數拓樸、代數幾何等領域的發展中自然出現的概念,主要的用途是用來高效地描述物之間的關係、用一種抽象的視角觀察整體性質。下面舉一些例子幫助你大致理解一下範疇論在數學領域的定位,看不懂或是想跳過都沒差,並不影響後續閱讀

Example 數學中的Categories與Functors [local-0]

  • 我們有所有集合與函數的範疇 Sets\text{Sets}、所有群與群態射的範疇 Groups\text{Groups}
  • Monoid MM 的Actions可以改寫成functor Mop→SetsM^{op} \to \text{Sets}(事實上這類functor有個特殊名稱,叫做presheaf)
  • 拓樸空間 XX 的所有開集之間的包含關係構成一個category Open(X)\text{Open}(X)
  • 遺忘functor,比如 Groups→Sets\text{Groups} \to \text{Sets}、Rings→Groups\text{Rings} \to \text{Groups}
  • Free functor,像是集合生成free groups/vector space等
  • 基本群可以視為從有點拓樸到群的functor π1:Top∗→Groups\pi_1 : \text{Top}_* \to \text{Groups}
  • vector space對偶可以視為functor (−)∗:vect(k)→vect(k)(-)^* : \text{vect}(\mathbb{k}) \to \text{vect}(\mathbb{k})
  • 從manifold構造它的ring of smooth functions可以看成functor C∞:Man→RingsC_\infty : \text{Man} \to \text{Rings},定義為 C∞(M):={f:M→R∣f is smooth}C_\infty(\mathbb{M}) := \{ f : \mathbb{M} \to \mathbb{R} \mid f \text{ is smooth}\}

什麼是色彩繽紛呢?這是指string diagram這種表達方式的圖,接下來我會用一種另類的category定義與string diagrams介紹範疇論。我盡量用不正式的語言,但直覺上正確的方式介紹,並且在每個概念附上一些案例連結作為參考。這表示我們會略過size問題假定自己在一個合適的宇宙上討論category theory

Definition Category的另類定義 [7U86]

一個category是由一些morphisms構成的,每個morphism都有頭跟尾兩個標籤(這些標籤又叫做objects),例如 f:A→Bf : A \to B 我們畫出直線並用一個點代表 ff

figure tex21528

當頭尾的標籤一樣的時候,我們可以把morphisms黏起來得到另一個morphism,例如 g:A→Bg : A \to B 而 f:B→Cf : B \to C,那 f∘gf \circ g 記成

figure tex21529

有一類特殊的morphism 1X:X→X1_X : X \to X,他們的標籤頭尾都一樣,跟其他任何morphism ff 組合都會是 ff(如果可以組合)。這類morphism有個特定的名稱,叫做identity morphism,而且對於該標籤 XX 這樣的morphism只有一個。對於這樣的morphism,我們用直線表示它可被忽略

figure tex21530

因為我們有等式 f∘1A=1B∘f=ff \circ 1_A = 1_B \circ f = f

figure tex21531

上面的表示法利用了functor space [1,C][1, \mathcal{C}](也可以記成 C1\mathcal{C}^1)的每個functor都指向某個object與其identity morphism的特性([1,C]≅C[1, \mathcal{C}] \cong \mathcal{C})濫用了記號,其實上面只是functor與natural transformation表示法的特例(但為了避免循環定義,所以還是會先定義category然後才討論functor與natural transformation)

Functor與Natural transformation [GC75]

Functor是範疇之間的變換,例如 F:C→DF : \mathcal{C} \to \mathcal{D} 可以記成

figure tex33460

Functor必須滿足 F(f∘g)=F(f)∘F(g)F(f \circ g) = F(f) \circ F(g),但這件事在我們目前用的string diagram表示法裡面看起來像是廢話所以就不畫了。Natural transformation是functors之間的變換,例如 α:F⇒G\alpha : F \Rightarrow G 可以記成

figure tex33461

Natural transformation必須滿足等式 G(f)∘αX=αY∘F(f)G(f) \circ \alpha_X = \alpha_Y \circ F(f),稱之為naturality

figure tex33462

用string diagram繪製之後,naturality就像是在說:我們可以把串珠在線上移來移去而不會改變意義。因此這又叫「電梯等式」,我們當然沒有必要侷限在objects跟morphisms上,所以更通用的形式是

figure tex33463

這種string diagram的語言在Introducing String Diagrams: The Art of Category Theory裡面有更多的介紹

Example 程式作為natural transformation [631K]

如果一些類型構成category,可以用functor表示參數化類型。例如我們可以定義natural transformation α:Maybe→List\alpha : \text{Maybe} \to \text{List} 為

αX:Maybe(X)→List(X)αX(Nothing)=[]αX(Some x)=[x]\begin{aligned} &\alpha_X : \text{Maybe}(X) \to \text{List}(X) \\ &\alpha_X(\text{Nothing}) &= [] \\ &\alpha_X(\text{Some}\ x) &= [x] \end{aligned}
figure tex19416

或是取出List中的最大值

figure tex19417

Interchange Law [ZB1T]

Adjunction [U27X]

Adjunction L⊣RL \dashv R 是範疇論中重要的概念,它是由一對functors L:C→DL : \mathcal{C}\to\mathcal{D} 跟 R:D→CR : \mathcal{D}\to\mathcal{C} 與一對natural transformations η:1C⇒R∘L\eta : 1_\mathcal{C} \Rightarrow R \circ L 與 ϵ:L∘R⇒1D\epsilon : L \circ R \Rightarrow 1_\mathcal{D} 組成。注意 1D1_\mathcal{D} 這種functor我們會乾脆不畫線

figure tex48236
Adjunction是可以串起來的

Example Limit與colimit [50LK]

每個 D:J→CD : J \to \mathcal{C} 有limit可以表示成diagonal functor Δ:C→CJ\Delta : \mathcal{C} \to \mathcal{C}^J 有right adjoint lim⁡:CJ→C\lim : \mathcal{C}^J \to \mathcal{C}。Δ\Delta 定義為 Δ(X)(j):=X\Delta(X)(j) := X

figure tex17727

對偶的來說,有colimit就是Δ\Delta有left adjoint colim\text{colim}

還有一些其他案例

用Adjunction構造Monad [9XIR]

有個跟adjunction緊密聯繫的概念是monad,我們這邊直接看adjunction如何構成monad。給定 L⊣RL \dashv R,有以下構造

figure tex22798

因此 R∘LR \circ L 是一個monad

偏數學的案例我目前只想得到adjunction構造的monad,在電腦科學裡面monad的用途是表示程式中的effects(參考 [Moggi1991])

更多string diagrams [local-1]

下一步呢? [local-2]

範疇論當然不是只靠一篇文章就能學會的,如果讀者有興趣的話,以下是根據不同背景我認為可能有用的學習資源

只是晚上睡不著?那也可以考慮Sketches of an Elephant

最後希望你閱讀的過程愉快,對範疇論更加了解了(或是我們可以開始談更高的廢話)

J-sheaf 的直覺 [LPLF]

JJ-sheaf 的概念有點難解,這個定義是在模擬從小的opens(局部)往大的opens(整體)前進的時候,每個在整體上的定義都可以分成局部的定義,而局部的定義黏起來會得到整體的定義這樣的匹配性,而且局部之間的交疊必須匹配。對於具體的sheaf,可以參考這裡

從topological space的opens構成的locale抽象化到category之後,我們就從opens變成操作objects;coverings變成sieves;在open XX 上定義的某類函數抽象化成presheaf,因為我們想讓框架適用於所有類型的函數,而且我們發現函數並不是必要的條件,我們只需要這些東西在某個意義上表現的像函數,因此presheaf僅僅是說,對於object XX 會有一個對應的集合,並且在morphism上我們contravariant(以符合幾何的直覺)。JJ-sheaf也因此等於在問,對於這樣抽象的 topology 與「函數」,什麽算是在交集處互相同意對方的值?

category中沒有交集的概念,sieve的composition closure確保了如果 f∈Sf \in S,那任何composable gg 有 f∘g∈Sf \circ g \in S,這就取代了交集扮演的角色

我們把 JJ-sheaf的定義攤開來變成圖:對每個 JJ-covering sieve SS 的每個元素 ff(它指出了一個更小的object)來說,都可以指定一個在更小的objects上定義的「函數」 xfx_f,而「函數」再往更小的地方切分的時候也依然跟 PP 互相協調,也就是條件 P(g)(xf)=xf∘gP(g)(x_f) = x_{f \circ g},這樣我們就成功的用category的語言描述了局部函數在更細的切分下彼此吻合的概念

figure tex6799
這張圖的下半部是 C\mathcal{C} 裡面的狀況 X←Y←ZX \leftarrow Y \leftarrow Z,上半部是 Set\text{Set} 裡面的狀況 {xf}→{xf∘g}{\{ x_f \}} \to {\{ x_{f \circ g} \}}。我用帶有圓圈的箭頭代表這些「函數定義在」object(open)上的概念

如果對每個滿足上述相容條件的family(即matching family),我們能夠確定整體上存在且只有唯一一個「函數」 x∈P(X)x \in P(X) 滿足這點的話,我們就說 PP 是一個 JJ-sheaf。這就是條件 P(f)(x)=xfP(f)(x) = x_f 的意義

Brid.gy cross-post 的使用方法 [CIEP]

因為我無法記住兩年前怎麼設定的 brid.gy,所以再次調查怎麼做然後寫下來。brid.gy 的首頁長這樣,像我是需要貼到 mastodon 所以就按 mastodon 按鈕

接著會出現兩個選項

我們這種用法要選的是 Cross-post to a Mastodon account @you@mastodon.server

輸入你的 mastodon instance,取得授權之後,你的文章裡面只要有 microformat 應該就能用用來發佈了

Soufflé 入門 [programming-0TXN]

Soufflé是高效能的Typed Datalog引擎,能把宣告式規則編譯成C++,或用直譯器模式執行

fact 與 rule [local-0]

fact(已知的事實)與 rule(能從事實推導新事實的規則)。先宣告一個輸入關係,代表「模組a直接相依於模組b」

// 代表 a 直接 import b
.decl depends(a: symbol, b: symbol)
.input depends

.decl 宣告關係的名字與各欄位型別(symbol 是字串,另一個常用型別是 number)。.input 表示資料從外部讀入:預設是從同名的 depends.facts 檔案,每行一筆、以 Tab 分隔。接著我們定義第一條規則:模組若被任何人相依,就是「被使用的」

.decl used(m: symbol)
used(m) :- depends(_, m).

讀法是「對任意 m,若存在 depends(_, m),則 used(m) 成立」。:- 右邊是body(前提),左邊是head(結論),_ 是 wildcard,表示「有就好、是誰都可以」。開發者不用管理控制流程:規則評估結果是順序無關的,由引擎自己算出所有成立的結論

Join [local-1]

Datalog的威力來自join,同一個變數出現在多個atom裡時,概念上是約束成同一個,會觸發unification。比如「a透過某個中介x間接相依於b」可以寫成

.decl indirect(a: symbol, b: symbol)
indirect(a, b) :-
  depends(a, x),
  depends(x, b).

x 在兩個 depends 裡都出現,引擎就通過「a依賴的東西」和「依賴b的東西」計算a到b的間接依賴

遞迴 [local-2]

但 indirect 只有 a -> b -> c 的情況會被辨識出來,相依是會傳遞的,不論中間隔幾層,可達性是經典的遞迴關係:transitive closure

.decl reachable(a: symbol, b: symbol)
reachable(a, b) :- depends(a, b).
reachable(a, b) :-
  depends(a, x),
  reachable(x, b).
  • base case:直接相依就是可達
  • induction case:a 依賴 x,x 遞迴可達的所有 b 都是 a 可達的

這裡的 reachable 只在 depends 既有的symbol之間關聯,不發明新值,可推導的fact是有限集,所以一定收斂

但保證終止不是Soufflé的通則。它有算術functor,只要規則會合成新值(如 A(i + 1) :- A(i).),domain就變成無限,會產生無窮迴圈,這種情況要自己加 i < 10 之類的條件控制

Stratified negation [local-3]

有了可達性,就能問「哪些模組是死碼」:不是從entry point可達的模組。這需要negation。先標出進入點,再定義「可從某entry到達」

.decl entry(m: symbol)
.input entry

.decl live(m: symbol)
live(m) :- entry(m).
live(m) :-
  entry(e),
  reachable(e, m).

.decl dead(m: symbol)
.output dead
dead(m) :-
  depends(m, _),
  !live(m).
dead(m) :-
  depends(_, m),
  !live(m).

!live(m) 是negation。Datalog的negation必須是 stratified(分層)的——不能有「自己依賴自己的否定」這種循環。Soufflé會把程式分層,確保 live 在被否定使用前已完全算完,!live 的語意才有良好定義

聚合:count、max、min、sum [local-4]

Datalog不只能推導關係,也能做聚合。例如算出每個模組「被多少人直接相依」,找出最該謹慎修改的核心

.decl fan_in(m: symbol, n: number)
.output fan_in
fan_in(m, n) :-
  used(m),
  n = count : { depends(_, m) }.

count : { depends(_, m) } 是「滿足 depends(_, m) 的fact有幾筆」。對固定的 m,引擎算出有多少個模組相依於它,綁定 n。Soufflé 也提供 max、min、sum,語法相同。聚合讓我們不必跳出Datalog就能回答「最多、最少、總共」這類問題

怎麼執行? [local-5]

把上面的規則存成 deps.dl,準備一個 facts/ 目錄放輸入:depends.facts 每行一筆 模組A⟨tab⟩模組B;entry.facts 列出進入點。然後在shell中輸入

souffle deps.dl -F facts -D out

-F 指定fact目錄,-D 指定輸出目錄。每個標了 .output 的關係跑完都會出現在 out/ 下:out/dead.csv 是deadcode的清單,out/fan_in.csv 是各模組的被依賴數量

以上是直譯模式,開發時使用。資料量大、要追求速度時,可以把程式編成原生執行檔再跑

souffle -o deps deps.dl
./deps -F facts -D out

理論部分參考 On Fast Large-Scale Program Analysis in Datalog

結論 [local-6]

Soufflé的心智模型很單純:我們描述「什麼成立」,引擎反覆套用推理規則直到不動點,算出所有能推導的facts

它的優勢在「對大量結構化資料反覆做join與遞迴查詢」:程式分析(points-to、call graph、taint)、相依與可達性分析、各種圖上的規則推導。當問題能轉換成「一組fact加一組蘊含式」,Soufflé就能精簡的表示邏輯,又不用損失執行速度

日本北陸之旅 2025/10 能登 [VW5V]

因為《放學後失眠的你》裡面的夜景(真脇遺跡)太漂亮了,所以我一直想要去能登一次

想不到再來發生了能登半島地震,直到這次旅程,能登半島沿途大部分的道路都還在整修中,有臨時紅綠燈跟工程人員管制(單側輪流通行),隨處可見大災難的痕跡

第一日(飛機/住宿) [hokuriku-day1]

在桃園機場隨意吃過午餐,坐上 12:10 起飛、往日本海岸線去的班機

在降落小松之前在日本海上轉向,往陸地飛去,經過軍機場之後 16:05 降落在小松機場。小松並不大,主要都在等過海關,下了樓之後因為接駁車等待人數不少,擔心可能要再等一班,於是坐計程車去小松車站,買了類似台灣區間車的車票做到金澤站

這台火車的座椅很有趣,有一個機械結構,乘客可以把座椅調整成對向四人座,或是兩人座。飯店 checkin 完畢再去找晚餐,這時候市場已經快關了,而且很多店大排長龍,就隨便找一間吃

第二日(能登第一日) [hokuriku-day2]

這天的目標是先去 ORIX 取車,然後開去千里濱凪沙道路,取車前還有一點時間,先跑去看鼓門

這一整日都陰雨綿綿,在取車之後在市區開了大概一個多小時之後來到千里濱,才發現因為海嘯的緣故,下去沙灘的道路全都封閉

前往下一個目標巖門,停好車之後,下去之前發現一隻大蜘蛛,所以就先拍了一些照片

巖門有一個比海更低的通道,漲潮的時候就無法通行,從通道看出去:

巖門園區裡面往沙灘的部分封閉。浮世繪

老鷹會在這個岩石上築巢

午餐吃能登牛富來本店,非常好吃,來到能登不要錯過

世界最長椅子:原本的椅子已經被摧毀,現在是新蓋的一座,現場有許多祈福話語

岸壁之母

附近商店裡用貝殼製作的精巧藝術

沿途的山路中有很多神社跟獨棟住宅,經過一座水庫

輪島漆芸美術館沒開,因為這樣也就跳過了輪島市,另外這裡受災也比較嚴重,不少店面都還沒有恢復,下次有機會再去看看。白米千枚田,目前景觀咖啡廳沒有營業

觀光製鹽景點,不過旅程時沒有營運

祿岡崎燈塔,現場有整修人員

青之洞窟還沒到關門時間,不過也沒有開放。他們的旅館:

瞭望台

這裏有賣超級昂貴的(靈能)石頭

晚間來到預定的小旅館 -- 珠洲温泉宝湯別館,旅館目前主要是維修工程人員住宿。由於已經比較晚了,所以詢問還能不能加訂晚餐,所以晚餐是家常料理風的炸豬排。房間就是榻榻米上面鋪棉被,這確實蠻硬的,不習慣會睡不好xd

溫泉是放到一個大約三人大小的浴池裡面,夜色從窗口透入,非常療癒

第三日(能登第二日) [hokuriku-day3]

早晨工程人員已經充充上工,宝湯別館提供的早餐:

與老闆夫妻聊天得知預期修復工程需要十年。早上的風景

宝湯別館

恋路海岸 • 弁天島

因為地震的關係,見附島現在禿了一塊

九十九灣。這裡遇到一車幼稚園小朋友,結果某個小朋友跑來跟我媽擊掌之後,後面每個孩子都決定要參一咖

真脇遺跡。時間安排的關係這次是白天去的,如果真的要晚上來要注意

  1. 這邊是山路而且沒燈,建議至少下午就來,然後等到晚上
  2. 好像是要跟管理處申請才能過夜

樹木恐龍!

午餐來到穴水町的能登前幸寿し。因為我媽不吃生魚片,本來討論等一下另外去附近店買別的吃,店主聽了之後特地卷了一道用蔬菜做的壽司

能登鐵道

有看到里山里海號但忘了拍照了xd

接下來從能登半島進入能登島,本來想說這次不去水族館,但因為算一算時間去飯店還太早,能登島上也沒有其他有興趣的景點,所以就決定去 Notojima 水族館

地上有投影的海洋生物,有一個孩子一直追著魚的幻影大叫「魚!魚!魚!」。整備中的萬聖節活動佈置

海豹很有表演慾,只要有人錄影就會一直跑來展現自己。這裡的冰淇淋店還提醒客人不要在室外吃,不然老鷹跟海鷗會搶xd

晚上來到七尾和倉,能登樂溫泉旅館

在吃晚餐之前有點時間,就跑去拍七福神雕像。這附近有一間台灣傳統美食,下次再去和倉一定會試試看xd

這次旅程的起因就到這裡,冰見、富山跟高岡其實也頗出乎意料的好玩,最後在金澤參觀一些著名景點,這部分就等我後續文章完成再釋出了

善與善的對抗 [BF9W]

討厭推薦演算法、討厭廣告、討厭 AI、討厭生活壓力、討厭別人、討厭自己

但行好事,莫問前程

願自己不要原地踏步。但善與善的對抗之中,什麼是好事?

violet literate programming 支援 [D1AX]

之前其實就有弄過一次但效果很不好,現在 LSP server 已經建立,基於 interaction 抽象層讀取資料產出 html 效果就好上很多

Example 初步實驗的效果 [violet-0000]

目前是用 violet weave 從一個 .vt.scrbl 編譯。程式碼區塊由 violet 前處理:@vt|{…}| 的內容會被抽出、型別檢查,轉成帶標記的 HTML。其餘內容原樣交給 tr-notes

把游標移到 identifiers 上時可以看到型別;調用部分連結到它的定義;?here 這樣的 goal 會顯示 context 與 target

\universe US U

\let id(A : universe U) -> (_ : A) -> A (Auniverse U : Uuniverse S U) : Auniverse U -> Auniverse U \where
  <= \intro
  | id A xA => xA

\let ex(A : universe U) -> (_ : A) -> A (Auniverse U : Uuniverse S U) : Auniverse U -> Auniverse U \where
  <= \intro
  | ex A xA => id(A : universe U) -> (_ : A) -> A Auniverse U xA

\let open-goal(A : universe U) -> (_ : A) -> A (Auniverse U : Uuniverse S U) : Auniverse U -> Auniverse U \where
  <= \intro
  | open-goal A xA => ?hereA : universe U
x : A
⊢ A

id 定義,ex 內用到了 id,open-goal 留了一個 ?here goal。

果然還是先做自己用得上的東西比較有趣xd

轉移控制權作為 effect:coroutine 與排程器 [Y7OR]

這是 effect 系列的第三篇,這篇要解釋怎麼用 effect 攔截 coroutines 的 yield 並重新排程。

所謂的 coroutines,就是一些互相約定好會主動讓出執行權利,互相「協作」的函數。是 continuation 極其常見的使用案例,首先我們先設定 abort 部分:

(define sched-tag (make-continuation-prompt-tag 'sched))

(define (yield)
  (call/cc
    (lambda (k)
      (abort/cc sched-tag k))))

可以看到 yield 就是預設外部會有一個 prompt (effect handler) 等著自己,於是它就把自己的 continuation 捕獲起來丟出去給 handler

  1. 我們需要一個地方存放還沒有做完的任務,所以定義了一個 queue tasks
  2. spawn 單純就是把任務推入 queue 中排隊
  3. continue 的目的是提供 handler 等待 thunk 的 yield 再次把執行權釋出
(define tasks (make-queue))

(define (spawn proc)
  (enqueue! tasks proc))

(define (continue thunk)
  (call/prompt
    thunk
    sched-tag
    (lambda (k)
      (enqueue! tasks k))))

最後就先把兩個 coroutines 推入 tasks,注意到兩個 tasks 只呼叫 spawn 並沒有意義,需要到下面的排程迴圈呼叫之後才會真的被執行

(spawn (lambda ()
         (for ([_n (range 5)])
           (displayln "A")
           (when (= 0 (random-natural 3))
             (yield)))))
(spawn (lambda ()
         (for ([_n (range 5)])
           (displayln "B")
           (when (= 0 (random-natural 3))
             (yield)))))

我們故意讓它隨機讓出執行權而不是每一步都讓出一次,在現實的使用情況中,我們通常是做了非常耗時的操作之後(比如各種 IO)才會讓出

不過在那種情況中我們這麼簡單的排程設計就有點問題了,我們會預期排程做更仔細的控制:handler 會在 coroutine 呼叫的資源準備好之後就把 coroutine 叫起來繼續,而不是直接把下一個 task 拿出來叫它繼續,確保 CPU 的利用率最大化

因為這只是單純教學,排程迴圈並不對 task 做出任何假設,也因此我們只用 queue 作為抽象結構,就像剛剛提到的一些策略,為了滿足那些策略,我們往往需要選擇合適的資料結構來滿足那些排程的需求。裡面放了一個 debug print 用來觀察排程過程剩餘的 tasks 的數量

(let loop ()
  (printf "How many tasks? ~a~n" (queue-length tasks))
  (cond
    [(queue-empty? tasks) (void)]
    [else
     (define t (dequeue! tasks))
     (continue t)
     (loop)]))

這樣的寫法當然有點樣板化,而且 effect handler 在這裏是不必要的,因為 yield 也能直接把自己的 continuation 推入 tasks,取出下一個任務跳轉:

注意到這裡 spawn 完還是需要自己記得從 tasks 中取一個出來執行才會開始執行
(spawn ...)
(spawn ...)
((dequeue! tasks))
(define (yield)
  (call/cc
    (lambda (k)
      (if (queue-empty? tasks)
        (k)
        (begin
          (enqueue! tasks k)
          (define t (dequeue! tasks))
          (t))))))

但這樣的話就需要每次都建立類似的樣板程式。用跳轉的方式解耦可以建立一個 macro 用來宣告區域,比如:

(with-scheduler
  (spawn ...)
  (spawn ...))

在該區域中的 spawn 不要直接壓入固定變數,而是從 macro 生成的 parameterize 取得要用的 tasks,這樣就可以把上面複雜排程迴圈就隱藏到 macro 中,使用者寫 coroutine,排程細節由 macro 負責

violet 的一點新進展 [ILIG]

申請了 domain https://violet-lang.org/ 給 violet 使用。比較大的變化是

  • Inductive types 可以生成相應的 eliminator 的類型與計算
  • 重新設計了 constructors 的 namespace,@ice1000 說的是對的,constructors 被放到 namespace 之下其實影響不大,要用時再打開,或是設計表面語法就可以了
  • 提供 \operator 讓開發者自己定義需要的運算式語法,比如
    \operator "\x + \y" => add x y
      \associativity: \left
  • 模組現在預設是 private,並新增 \export 關鍵字
  • 建立了 project 概念,現在可以用 info.vt 管理專案依賴,進一步可以參考 https://github.com/violet-prover/std
  • kernel 加入內建的 record

下一步? [local-0]

現在的 <= \elim 並不處理有任何帶有非純變數參數的 elimination target,因為這會導致 eliminator 有些 case 完全沒有意義,需要引入等式去除不可及的分支。另一個辦法是讓開發者提供 motive,但那樣就沒幫助到開發者了xd

另外 lock 檔案的鎖定實現還不完整,這些實務上的限制會是接下來需要仔細處理的問題

用 effect system 實現直釋器 [SW6P]

這是 effect 系列的第二篇,這篇要展示的是用 effect 攔截對 environment 的查詢。所以第一步我們要定義一個攔截 effect

(define lookup-tag (make-continuation-prompt-tag 'lookup))
(define (lookup x)
  (call/cc
    (lambda (k)
      (abort/cc lookup-tag k x))))

第二步我們建立執行函數 EVAL,但我們建立一個 go 輔助函數,讓最上層負責攔截找不到 variables 時的查找,並提供沒有該 variable 的錯誤訊息。這裏跟一般直譯器實現的主要差異就是我們不需要傳 env 這樣的變數,這通常是某個 environment 實現(比如 hashmap 或是一些值構成的 list,由 de Bruijn indicies 參照)的實例

(define (go tm)
  (match tm
    [(? symbol? tm) (lookup tm)]
    [(? number? tm) tm]
    [`(+ ,@a) (apply + (map go a))]

綁定變得很容易 [local-0]

可以看到,綁定就變成只是放一個 handler 攔截,遇到查詢就把記錄下來的值丟給 resume 跳回去

    [`(let [,x ,v] ,tm)
     (call/prompt
       go lookup-tag
       (lambda (resume x1)
         (if (eq? x x1)
             (resume (go v))
             (resume (lookup x1))))
       tm)]

那 closure 就變得有點麻煩 [local-2]

但相應的,比起明確在參數中提供 env 變數的版本,我們沒辦法直接把 env 複製進 closure 就好,而是需要特地找出 free variables 並在當前的環境中找到這些變數然後複製成一個 environment。這正是 Lexical scoping與dynamic scoping的對偶的實際案例

    [`(lambda (,x) ,body)
     (define fvs (remove x (remove-duplicates (free-vars body))))
     (closure x body
              (for/list ([y fvs]) (cons y (lookup y))))]

在 application 的部分就需要把存起來的 environment 播放出來變成一系列的 handler

    [`(,f ,a)
     (define fv (go f))
     (define av (go a))
     (match fv
       [(closure x body env)
        (install-env (cons (cons x av) env) body)])]))

closure 的輔助函數 [local-1]

(struct closure (param body env) #:transparent)

(define (free-vars tm)
  (match tm
    [(? symbol? x) (list x)]
    [(? number?) '()]
    [`(+ ,@as) (append-map free-vars as)]
    [`(let [,x ,v] ,t)
     (append (free-vars v) (remove x (free-vars t)))]
    [`(lambda (,x) ,body)
     (remove x (free-vars body))]
    [`(,f ,a)
     (append (free-vars f) (free-vars a))]))

(define (install-env bs tm)
  (match bs
    ['() (go tm)]
    [(cons (cons x v) rest)
     (call/prompt
       (lambda () (install-env rest tm))
       lookup-tag
       (lambda (resume y)
         (if (eq? x y) (resume v) (resume (lookup y)))))]))

EVAL 用來提供錯誤訊息

(define (EVAL tm)
  (call/prompt
    go
    lookup-tag
    (lambda (_resume x)
      (printf "~a is undefined ~n" x))
    tm))

最後我們確認它確實能執行出正確的結果

> (EVAL
    '(let [x 1]
       (let [f (lambda (y) (+ x y))]
         (f 10))))
11

正確處理單位的算術系統 [S20M]

在做物理或工程上的算術時,我們常常會在單位上出錯:把公尺加到秒上、把公斤乘進長度卻沒有標記,或是次方算錯,最後得到一個「數字看起來合理」但單位錯亂的結果。這些錯誤在紙筆運算時不容易被察覺,發現的時候往往已經是好幾步之後,回頭檢查非常的麻煩

能不能讓工具替我們把關呢?如果單位本身就是型別的一部分,編譯器就能在我們把不相容的東西硬湊在一起時直接拒絕。我們可以用 Agda 來規範出這樣的一個系統

用 Agda 實現單位正確的算術 [ag-OW40]

{-# OPTIONS --safe --without-K #-}
module ag-OW40 where

open import MLTT.Spartan hiding (_+_)
open import MLTT.Negation
open import MLTT.List
open import MGS.MLTT using (has-decidable-equality)
open import Integers.Type
open import Integers.Addition
open import Integers.Multiplication

首先我們建立一些單位

data Unit : 𝓤₀ ̇ where
  Meter Kilogram Second : Unit

單位的相等是可判定的

Unit-≟ : has-decidable-equality Unit
Unit-≟ Meter    Meter    = inl refl
Unit-≟ Kilogram Kilogram = inl refl
Unit-≟ Second   Second   = inl refl
Unit-≟ Meter    Kilogram = inr (λ ())
Unit-≟ Meter    Second   = inr (λ ())
Unit-≟ Kilogram Meter    = inr (λ ())
Unit-≟ Kilogram Second   = inr (λ ())
Unit-≟ Second   Meter    = inr (λ ())
Unit-≟ Second   Kilogram = inr (λ ())

整數的相等也是可判定的,TypeTopology 已經有 ℤ-is-discrete,這裡我們給它一個新名字,好跟 Unit-≟ 慣例相符

ℤ-≟ : has-decidable-equality ℤ
ℤ-≟ = ℤ-is-discrete

完整的單位簽名由一系列單位與整數組成,整數用來表示該單位的次方,比如公尺的 3 次方就是 [(Meter , pos 3)]

Units : 𝓤₀ ̇
Units = List (Unit × ℤ)

現在讓任何型別 T 都可以附帶單位,但 runtime 只存放 T

data _with-unit_ (T : 𝓤 ̇ ) (us : Units) : 𝓤 ̇ where
  Box : T → T with-unit us

接下來我們要對單位進行乘法,方法就是同單位的次方抵銷。這個合併演算法可以大致描述成

  1. 要是紀錄的單位相同,就進行相加並放回清單。除非相加結果為 pos 0(也就是次方為 0),這時候就直接從清單中消去
  2. 否則就繼續向後尋找可能的合併目標
  3. 要是都找不到,就把自己插在最後面
*-unit : Units → Units → Units
*-unit us₁ us₂ = foldr step us₁ us₂
  where
  step : Unit × ℤ → Units → Units
  step (u , n) []             = (u , n) ∷ []
  step (u , n) ((v , m) ∷ xs) with Unit-≟ u v
  ... | inr _ = (v , m) ∷ step (u , n) xs
  ... | inl _ with ℤ-≟ (n + m) (pos 0)
  ...   | inl _ = xs
  ...   | inr _ = (u , n + m) ∷ xs

現在開始實現帶有單位的整數運算。相加只能跟同單位的數一起,所以打從一開始沒辦法寫錯把不同單位的數字加在一起

_+̇_ : {us : Units} → ℤ with-unit us → ℤ with-unit us → ℤ with-unit us
Box v₁ +̇ Box v₂ = Box (v₁ + v₂)
infixl 40 _+̇_

乘法涉及到單位的相乘(正確的處理次方抵銷問題)

_×̇_ : {us₁ us₂ : Units}
    → ℤ with-unit us₁
    → ℤ with-unit us₂
    → ℤ with-unit (*-unit us₁ us₂)
Box v₁ ×̇ Box v₂ = Box (v₁ * v₂)
infixl 40 _×̇_

最後來看一些範例,這裏把 3s + 3s 所以得到 6s

test-3s+3s : ℤ with-unit [(Second , pos 1)]
test-3s+3s = s +̇ s
  where
  s : ℤ with-unit [(Second , pos 1)]
  s = Box (pos 3)
is-6s : test-3s+3s = Box (pos 6)
is-6s = refl

這裏把 7m³/(kg·s²) 乘上 2s,所以得到 14m³/(kg·s)

test-7m³/kg·s²×2s : ℤ with-unit ((Meter , pos 3) ∷ (Kilogram , negsucc 0) ∷ (Second , negsucc 0) ∷ [])
test-7m³/kg·s²×2s = g ×̇ s
  where
  g : ℤ with-unit ((Meter , pos 3) ∷ (Kilogram , negsucc 0) ∷ (Second , negsucc 1) ∷ [])
  g = Box (pos 7)
  s : ℤ with-unit [(Second , pos 1)]
  s = Box (pos 2)
is-14m³/kg·s : test-7m³/kg·s²×2s = Box (pos 14)
is-14m³/kg·s = refl

不是具體的值當然也可以應用這套系統

test-*-abstract : ℤ with-unit ((Meter , pos 4) ∷ (Kilogram , negsucc 0) ∷ (Second , negsucc 1) ∷ [])
                → ℤ with-unit [(Second , pos 1)]
                → ℤ with-unit ((Meter , pos 4) ∷ (Kilogram , negsucc 0) ∷ (Second , negsucc 0) ∷ [])
test-*-abstract a b = a ×̇ b

Layering 系統 [local-0]

其實上面的概念來自 Software Design for Flexibility,書中把這個技巧稱之為 layering。書中的程式長得像這樣:

(define G
  (layered-datum 6.67408e-11
    unit-layer (unit 'meter 3 'kilogram -1 'second -2)))

用我們熟悉的系統表示:

m3kg⋅s2 \frac{m^3}{kg \cdot s^2}

書中採用的技術對大部分語言來說都是適用的,只要能表達附註屬性在資料上就可以了。OOP 語言可能就會定義一個專有的 class 表示 layered data。這裡用標記的方式的優勢也一覽無遺,畢竟大部分語言的型別系統通常並不像上面的 Agda 有這麼強力的能力做這麼複雜的限制

書中的系統的缺點是難以驗證,要是 layering 的儲存單元沒有正確操作呢?相比於依賴型別論的檢驗,手工制作這樣的系統更難被相信沒有疏失。但除了用 Agda(或類似的語言),還有什麼辦法可以讓我們相信規範的正確性呢?

我在 2022 年的時候認為採用 cartesian product 存下所有輸入,組合 AST 時驗證等到最後才計算很不合理。但其實這應該是在沒有高複雜度的型別系統時應該採取的設計(其實就是某種直譯器模式變種)。像這樣臨時附加上的型別系統通常足夠小,足以通過原始碼直接判斷正確性,又不需要因此採用其他語言。所以採用這樣的方法除了稍微不直覺跟一點 runtime 開銷,在正確性很重要時也沒什麼。不過反過來說 layering 系統一般來說都需要額外付出 runtime 檢查開銷,所以除非確定正確性是必要條件,否則這大概也不會是好的實現方法

到這裡你應該已經可以想出在自己最熟悉的語言裡面要怎麼寫出類似的程式,因為 layering 的概念其實並不陌生,在不同的程式語言裡面有諸多實現不同種類的限制的方法。最常見的一種就是 type system,其他像是 eBPF 的 logging 與 process 分開也能算是這樣的工具, 甚至是最簡單的 assertion 也能幫助我們進一步對程式做更多的推斷

以 effect 為合約:用 effect system 解耦遍歷演算法與存取點 [4Z05]

我以前寫過一篇文章介紹 effect system,看過之後的回饋基本上有兩種

  1. 哇靠這也太抽象了,到底在說什麼
  2. 那 effect system 到底對我們日常的程式開發有什麼用?

所以我打算利用零碎的時間寫寫各種應用範例,而這篇就是關於如何用 effect system 將遍歷演算法與 visit 處理分離。我想不重複 racket 中的有界續延如何實作 effect system 的功能?已經介紹過的各個 racket API,所以直接用程式案例說明。比如今天我們想要走訪 binary tree,以 inorder 走訪可以寫成

(define (dfs t)
  (match t
    [(list v L R)
     (dfs L)
     (visit v)
     (dfs R)]
    [v (visit v)]))

我們單純認定 (list root-value L-tree R-tree) 這樣的格式是 binary tree,這很可能不是最好的寫法,不過以這裡要講的概念來說,重點在 visit 是什麼,所以我們先看一下實現

要記得用 (require racket/control) 引入 racket/control
(define visit-tag (make-continuation-prompt-tag 'visit))
(define (visit v)
  (call/cc
   (lambda (k)
     (abort/cc visit-tag k v))))

這段程式大致上就是說,abort/cc 會去找 visit-tag 的 handler 處理(這部分跟 try/catch 與 raise 是完全一樣的,raise 就是 abort/cc)。而 call/cc 提供了 k 這個 continuation,而且我們也把它傳給 handler,讓 handler 知道怎麼跳回來,最後把 v 傳給 handler。

調用 dfs 的程式需要準備好 handler。call/prompt 的格式是:函數、prompt tag、handler、函數的參數。以下範例把節點的值印出來

(call/prompt
  dfs
  visit-tag (lambda (resume v)
              (println v)
              (resume))
  '(4 (2 1 3) 5))

換一個 handler,完全不需要動 dfs 的程式,就能改成計算節點數量

(define counter 0)
(call/prompt
  dfs
  visit-tag (lambda (resume v)
              (set! counter (add1 counter))
              (resume))
  '(4 (2 1 3) 5))
(println counter)
這裡用全域變數 counter 是為了簡化範例

如果不用 effect system,遍歷演算法就得有 visitor 這個 callback 參數,或是直接 hardcode 每個存取點的操作。後者很容易發生:開發者順手寫死,等到要改的時候才發現需要四處找出調用點、改寫成 callback 形式。

callback 法還有另一個深層問題:決定 callback 行為的地方,有時候在呼叫遍歷演算法的好幾層 frame 之上。這樣一來,中間穿過的每一個函數都得多帶一個參數,純粹為了傳遞這個 callback。effect system 找 handler 的方式是從執行期的 call stack 往上翻,所以 handler 可以放在任何外層 frame,完全不需要靠參數傳遞。

使用 effect system 之後,遍歷演算法不需要知道實際上 visit 會進行什麼操作,只需要相信 visit 會去找某個 handler 做事;而 visit 的 handler 也不用知道遍歷演算法是怎麼實現的,只需要相信遍歷演算法會把結構的每個節點的值傳給自己。兩方完全依賴 visit 為合約!

RSS in emacs:elfeed [EHWW]

I've been trying some Emacs packages, including elfeed and meow, but the latter is not the topic of this post. I will briefly introduce elfeed package.

I use straight.el to install packages:

(use-package elfeed
  :custom
  (elfeed-feeds
   '("https://dannypsnl.srht.site/rss.xml")))

elfeed-feeds is where you configure the feeds. Here I use my personal site as an example. Use M-x elfeed to open a buffer of elfeed. In this buffer, pressing G will fetch and refresh the feeds. Once articles are listed, pressing Enter will open the selected one. In reading mode, n and p move to older and newer articles, respectively.

Pattern Unification演算法理論部分 [F6C1]

在這篇文章裡面我想要探討 Elaboration Zoo 裡面的 pattern unification 演算法怎麼算出來的,以及在什麼時候成立。主要是了解我們怎麼導出等式:

?α=?rhs′[spm−1] ?\alpha \overset{?}{=} \text{rhs}'[sp_m^{-1}]

超越 Elaboration zoo 中的合一演算法與隱式參數 僅是直覺的推理。在 Display map category 我們已經了解到,contexts 跟 substitutions 構成了一個範疇,而我們想要知道的是 epimorphism 的定義,所以 Kovacs 的第一步是把 epimorphism 定義改寫成更適合這個案例的定義

Definition Epimorphism [local-0]

我們說 Sub Γ ΔSub\ \Gamma\ \Delta 是 epimorphism iff

δ∘σ=δ′∘σ⇒δ=δ′A[σ]=A′[σ]⇒A=A′\begin{aligned} \delta \circ \sigma = \delta' \circ \sigma \Rightarrow \delta = \delta' \\ A[\sigma] = A'[\sigma] \Rightarrow A = A' \end{aligned}

這也導出了 term 的事實:u[σ]=u′[σ]⇒u=u′u[\sigma] = u'[\sigma] \Rightarrow u = u'

我們關心一個 substitutions 中的子集:renamings

Definition Renamings [local-1]

一個 substitution σ:Sub Γ Δ\sigma : Sub\ \Gamma\ \Delta 如果純粹由變數構成,那麼它是 renaming,記成 σ:Ren Γ Δ\sigma : Ren\ \Gamma\ \Delta

而且 Renaming 有以下特性

  1. 如果只用到 Γ\Gamma 中的變數至多一次,那麼 σ\sigma 是 epimorphism
  2. 如果只用到 Γ\Gamma 中的變數至少一次,那麼 σ\sigma 是 monomorphism
  3. 如果只用到 Γ\Gamma 中的變數剛好一次,那麼 σ\sigma 是 isomorphism(context 的 permutation)

Definition Embeddings [local-2]

Renamings 還有一個子集叫 Embeddings,是 Δ\Delta 為 Γ\Gamma 捨棄 0 到多個變數後組成的 context。

  1. 每個 embedding 都是 epimorphism
  2. embedding Γ→Γ\Gamma \to \Gamma 一定是 identity

Renaming σ\sigma 總是能被分解為(這個動作叫 strengthening,可以延伸到 type/term 的 substitution 上)

σm∘σe=σ\sigma_m \circ \sigma_e = \sigma

其中 σm\sigma_m 是一個 monomorphism,而 σe\sigma_e 是一個 embedding

Pattern unification 問題是,在某個 context Γ\Gamma 下有等式

Γ⊢?α[sp]=?rhs \Gamma \vdash ?\alpha [sp] \overset{?}{=} \text{rhs}

其中

  • Δ⊢?α:A\Delta \vdash ?\alpha : A
  • sp:Sub Γ Δsp : Sub\ \Gamma\ \Delta
  • Γ⊢rhs:A[sp]\Gamma \vdash \text{rhs} : A[sp]

所謂的 pattern conditions 是以下 3 個條件

  1. spsp 是一個 epimorphism 跟 renaming,可以分解成 spm∘spesp_m \circ sp_e
  2. rhs=rhs′[spe]\text{rhs} = \text{rhs}'[sp_e]
  3. α\alpha 這個 meta variable 在 rhs\text{rhs} 中不是自由變數

spsp 是 epimorphism 意味著 spmsp_m 也是 epimorphism,因此 spmsp_m 是 isomorphism,具有 inverse spm−1sp_m^{-1}。從初始問題出發

?α[sp]=?rhs ?\alpha [sp] \overset{?}{=} \text{rhs}

分解 spsp 得到

?α[spm∘spe]=?rhs ?\alpha [sp_m \circ sp_e] \overset{?}{=} \text{rhs}

替換 rhs\text{rhs} 得到

?α[spm][spe]=?α[spm∘spe]=?rhs′[spe] ?\alpha [sp_m][sp_e] = ?\alpha [sp_m \circ sp_e] \overset{?}{=} \text{rhs}'[sp_e]

embedding 是 epimorphism

?α[spm]=?rhs′ ?\alpha [sp_m] \overset{?}{=} \text{rhs}'

套上 spm−1sp_m^{-1}

?α[spm∘spm−1]=?α[spm][spm−1]=?rhs′[spm−1] ?\alpha [sp_m \circ sp_m^{-1}] = ?\alpha [sp_m][sp_m^{-1}] \overset{?}{=} \text{rhs}'[sp_m^{-1}]

inverse 對消

?α=?rhs′[spm−1] ?\alpha \overset{?}{=} \text{rhs}'[sp_m^{-1}]

這促使我們定義 ?α:=rhs′[spm−1]?\alpha := \text{rhs}'[sp_m^{-1}],然後就只需要檢查 ?α?\alpha 不是解的自由變量就可以了,原因參考 Elaboration with first-class implicit function types 的細節。直覺一點來說,本來的假定條件會導出 α\alpha 不是自由的,如果檢查卻發現它是自由的,就表示假定條件不成立。

最後

  1. 判斷 Renaming 是不是 epimorphism
  2. 分解 Renaming
  3. type/term 的 strengthening(如果 type theory 是 normalizing)

這三個演算法都是可判定的,因此 pattern unification 這個演算法是可判定的。而且由於分解唯一,解也是唯一的。

安裝 Guix [B4OI]

下載之後按照系統安裝文件描述的方式把 iso 燒錄到 USB or DVD,插入電腦開始安裝。基本上現在的引導介面已經跟 ubuntu 之類的老牌 distribution 相當了,只要按照螢幕上說的逐步回答就能設定完成。

它的 root 跟一般帳號是明確分開的,密碼也不同,在設定完 root 密碼之後會要求建立至少一個一般帳號。partition 分成兩種,按照需求選擇就好。最後選介面時有很多選擇,Gnome、KDE、Sway 都有,這個也是習慣用什麼就選什麼就行了。這之後就會開始安裝,跑完之後重開機(boot 管理要記得切換到安裝好的系統才不會又跑進 installer)。

到這裡就可以看 https://guix.gnu.org/manual/1.5.0/en/html_node/Getting-Started-with-the-System.html 文件開始編輯 /etc/config.scm 了。

2026 - Week 5 [2026-W5]

Racket 中的 concurrent programming [local-0]

想到就測試 rakka 跟 Racket coroutine thread 結果 native 還比較快XD,所以我就把 genserver 做成 Racket thread 封裝了 https://repo.dannypsnl.me/dannypsnl/erl。至於為什麼 coroutine thread 這麼快,我猜可能 CS 之後有很大改善,我之前沒有注意到這點

JIT [local-1]

Racket language server [local-2]

racket-langserver 這個 PR 的目的是改善目前一團糟的測試體驗。現在的方式是啟動 subprocess 然後跟它的 stdio 互動,一來這意味著大量的系統資源需求,二來耦合這麼重就很難設計可以遠端連線的 server。所以第一步需要清理現在的設計,引入間接層讓測試只需要跟這個抽象部分互動就好

racket-llvm [local-3]

2026 - Week 4 [2026-W4]

Rakka 使用初體驗 [GMYW]

最近發現 racket 生態系裡面出現一個 Rakka 程式庫,它似乎把 Erlang 大部分一階抽象都做出來了,這是我個人想像很久的寫法,所以我當然很有興趣去嘗試。我的第一個實驗是針對的 sauron 內部程式庫 collector 進行重構。collector 原本就是模仿 Erlang 的訊息傳遞方式來抽象,使用 rakka 之後,我直接把這些抽象邏輯寫成 GenServer 的形式。結果發現效果非常不錯

  1. 程式碼數量減少:collector 的程式碼量變成原來的大約三分之二。
  2. 維護性提升:整體結構變得更好維護。

所以之後遇到類似的需求,我就會考慮使用 rakka 處理,我就不用再寫處理訊息傳遞風格的樣板式重複程式碼了。而且它還實現了 EPMD protocol,可以跟其他 node 互動。

Display map category 捕捉了類型論的什麼面向? [tt-BBGG]

要理解 Display map category 的定義(參考 Categorical Logic and Type Theory, Definition 10.4.1),我們需要理解它的動機跟捕捉問題的方式,因此我們要觀察一個個相繼式(我假設讀者已經熟練相繼式定義的類型論),並用範疇論(category theory)的語言重述。

在用相繼式描述類型論時,公式

Γ⊢A type\Gamma \vdash A \text{ type}

表示 Γ\Gamma 可派生出類型 AA。對於所有 Γ\Gamma 可派生的類型,我們用 Ty(Γ)Ty(\Gamma) 表示,所以我們也會用 A∈Ty(Γ)A \in Ty(\Gamma) 來表示上面的相繼式。Display map category 捕捉的第一件事就是 contexts 構成一個 category,我們用 C\mathcal{C} 記號表示這個範疇,並且我們要求 C\mathcal{C} 有 terminal object ⋄\diamond,用來表示空的 context。

context 的構造方式只有兩種:⋄\diamond 是一個 context;context Γ\Gamma 加入一個類型 AA 得到的 Γ▹A\Gamma \triangleright A 還是 context。

因此 Γ▹A▹B▹⋯\Gamma \triangleright A \triangleright B \triangleright \cdots 也常被稱為 telescope

在相繼式中如果我們寫

σ:Δ\sigma : \Delta

我們的意思是 Δ=⋄▹T1▹⋯▹Tk\Delta = \diamond \triangleright T_1 \triangleright \cdots \triangleright T_k,對於每一個 TiT_i,都能找到 σi:Ti\sigma_i : T_i,我們稱「σ\sigma 實現了 Δ\Delta」。

但上面那樣寫有問題,因為 σ\sigma 沒有憑依,因此正確的通用寫法是記錄成

Γ⊢σ:Δ\Gamma \vdash \sigma : \Delta

這表示 σ\sigma 參考 Γ\Gamma 中的變數,並實現了 Δ\Delta,這叫做一個替換。

對 type 套用替換 [local-0]

如果我們同時有 Γ⊢σ:Δ\Gamma \vdash \sigma : \Delta 跟 Δ⊢A type\Delta \vdash A \text{ type},那我們可以對 AA 進行替換得到:

Γ⊢A[σ] type\Gamma \vdash A[\sigma] \text{ type}

換句話說,我們用 σ\sigma 完成了一個逆變,記成 σ∗:Ty(Δ)→Ty(Γ)\sigma^* : Ty(\Delta) \to Ty(\Gamma)。

對 term 套用替換 [local-1]

用相繼式時

Γ⊢u:A\Gamma \vdash u : A

表示 uu 在 Γ\Gamma 中有類型 AA,當然,這裡隱含的前提是 A∈Ty(Γ)A \in Ty(\Gamma)。

而替換同樣可以在 term uu 上作用:

Γ⊢σ:ΔΔ⊢u:AΓ⊢u[σ]:A[σ]\frac{ \Gamma \vdash \sigma : \Delta \quad \Delta \vdash u : A }{ \Gamma \vdash u[\sigma] : A[\sigma] }

對替換套用替換 [local-2]

但我們也知道替換 σ\sigma 也能看成一個 term,所以同樣可以對它套用替換!

Γ⊢σ:Γ′Γ′⊢γ:Γ′′Γ⊢γ[σ]:Γ′′\frac{ \Gamma \vdash \sigma : \Gamma' \quad \Gamma' \vdash \gamma : \Gamma'' }{ \Gamma \vdash \gamma[\sigma] : \Gamma'' }

這說明了替換可以結合!我們經常記成 γ∘σ\gamma \circ \sigma。

而很明顯的,對於所有 Γ=⋄▹T1▹⋯▹Tk\Gamma = \diamond \triangleright T_1 \triangleright \cdots \triangleright T_k 有一個「引用變數 Γi\Gamma_i 來實現 TiT_i 的不變替換 idΓid_\Gamma」,所以我們想把每個替換 σ\sigma 視為 C\mathcal{C} 中的 morphism σ:Γ→Δ\sigma : \Gamma \to \Delta。這裡需要證明幾件事

  1. 對於給定的 Γ,Δ\Gamma, \Delta,可能的 Γ→Δ\Gamma \to \Delta 的 collection 是一個集合。由於所有替換都涉及對 Γ\Gamma 的引用(或是有限多的類型生成規則)並做出一個有限構造,所以可以用歸納的方式比較,這讓證明是唯一的,因此這些替換是 set
  2. 對 f:Γ→Δf : \Gamma \to \Delta 來說有 f∘idΓ=f=idΔ∘ff \circ id_\Gamma = f = id_\Delta \circ f。根據定義就知道了
  3. 替換要有 associative f∘(g∘h)=(f∘g)∘hf \circ (g \circ h) = (f \circ g) \circ h。這個不好證明,就我所知都是對規則進行 induction 得到(參考 Substitution Without Copy and Paste 的 §4.3)

現在我們要進入 Display map 的部分

Definition Display map [local-3]

對於 Γ\Gamma 與 A∈Ty(Γ)A \in Ty(\Gamma),我們有 πA:Γ▹A→Γ\pi_A : \Gamma\triangleright A \to \Gamma,這叫做 display map。提醒一下這個相繼式是

Γ,A⊢πA:Γ\Gamma, A \vdash \pi_A : \Gamma

考慮逆變,就得到

πA∗:Ty(Γ)→Ty(Γ▹A)\pi_A^* : Ty(\Gamma) \to Ty(\Gamma \triangleright A)

唯一確定了一個類型,也就是說現在每個 type AA 都被一個某一些替換代表了。這也可以說其實我們在看 HomC(−,Γ)Hom_\mathcal{C}(- , \Gamma) 這些 morphisms。

term 的表示 [local-4]

同理可以放到 term uu 上,對於

Γ⊢u:A\Gamma \vdash u : A

可以視為

Γ⊢(idΓ,u):(Γ,A)\Gamma \vdash (id_\Gamma, u) : (\Gamma, A)

也就是說每一個 term uu 都可以看成替換

Γ→(idΓ,u)Γ▹A\Gamma \xrightarrow{(id_\Gamma, u)} \Gamma \triangleright A

到這裡我們發現,這個模型很好的捕捉了 syntactic categorical 的觀點,把類型論的句法特性都保留了下來。前面說過每個替換都有逆變,現在我們要證明逆變是一個 functor:

Proposition 逆變是 functor [local-6]

對於任何 σ:Δ→Γ\sigma : \Delta \to \Gamma,我們把逆變看成 slice category 的函數 C/Γ→C/Δ\mathcal{C} / \Gamma \to \mathcal{C} / \Delta,要檢查這是否是 functor,我們想要證明兩件事:保留 identity 且保留 composition。

Proof [local-5]

套上 display maps 的規範,知道逆變會產出一個 pullback:

figure tex10599

這直接證明了 σ∘σ∗(id)=σ\sigma \circ \sigma^*(id) = \sigma,所以 σ∗(id)=idΔ\sigma^*(id) = id_\Delta,表示 σ∗\sigma^* 保留了 identity。

要證明 composition,可以用 pullbacks 的結合知道如果 σ∗(f)∘σ∗(g)\sigma^*(f) \circ \sigma^*(g) 跟 σ∗(f∘g)\sigma^*(f \circ g) 的頂點 up to isomorphism 是同一個,因此這兩個 morphism 是一樣的,證明了也保留 composition。因此每個替換誘導出的逆變都是 functor。 □

既然 σ∗\sigma^* 是個 functor,那在 display map category 裡,我們可以用 σ∗\sigma^* 的 right adjoint 表示 product,而且加上 Beck-Chevalley 條件:存在一個 natural isomorphism,讓我們可以把 product 用替換推來推去而不會變成不同的東西。

weak sum 同理,只是變成 σ∗\sigma^* 的 left adjoint,同樣要有 Beck-Chevalley 條件保證 coproduct 可以被推來推去而不遺失。strong sum 則是把條件強化到 display maps 組合一定是 display map,這蘊含 weak sum 條件。

我們前面完全沒有提到 Γ⊢a=a:A\Gamma \vdash a = a : A 這種形式,而確實這是 display map categorical 方法的額外條件:weak equality 用 diagonal 的 left adjoint EqπAEq_{\pi_A} 表示,還是用 Beck-Chevalley 條件避免遺失。strong equality 規定 diagonal 是 display map,蘊含 weak equality。

對細節有興趣的讀者可以看 Paige Randall North 的 slide:Homotopical models of type theory

現在我們看到,display map category 除了捕捉 type theory 的 syntactic 特性,還可以用直觀的範疇論語言表述 type theory 的數種特徵,成功的讓我們用範疇論的觀點來檢視類型論。

現代 - 商業、科技與政治交織的空間 [U5YI]

讓我們先從 LLM 開始談起,我認為 Context Widows 的觀點很適合開始理解問題在哪裡。問題不在 LLM 或是廣泛的 AI 領域有用或是沒用,正如 Context Widows 一文自己所說跟我從其他地方看到的,正面的產出確實存在,像是

  1. 蛋白質折疊
  2. 材料科學
  3. 用壓縮映射模擬星系演變

而可質疑的問題也一直存在:幻覺、缺乏理解。更不用說直接的負面影響

  1. 氣候
  2. 佔用能源(包含在天災時電網供電給計算中心而不是醫院)
  3. 剽竊創意

Context Widows 真正要談的是,比起問 LLM 有沒有用,更恰當的問題是:LLM 能否有效地融入科學知識的創造、驗證和傳播過程中?

LLM 能否有效地融入科學知識的創造、驗證和傳播過程中? [local-0]

由於一項技術的潛在用途並非其實際用途,而實際用途又取決於採用該技術的機構、機構試圖解決的問題以及機構用作衡量成功的既有指標。所以 Context Widows 認為探討 LLM 與科學的關係,等若詢問 LLM 出現之前,科學界已經運作著怎樣的體系?

我們知道當代科學界的主導是引用體系,隨著時間發展,引用體系已經改變它本要量測的對象;科學家會調整自己的行為以最佳化指標。Context Widows 也駁斥用古德哈特定律「當一個指標成為目標時,它就不再是一個好的指標」來解釋這件事,因為古德哈特定律只認為指標是問題,但 Context Widows 一文認為有問題的是需要指標才能運作的組織形式。

在這個背景下,LLM 雖然有其他的功用,卻只會加速學術界的固有邏輯:更多的發表。引用文中的話來說

LLM 並非經濟學家(一群熱衷於用貶低維度的方式來消除意義的人)所稱的科學激勵「結構」的創造者。相反,這項技術恰好出現在這樣的背景下,提供了一種更快的方式來生產系統所獎勵的成果。

而很明顯的,這不局限於學術界中,搜尋引擎的誕生改變了網路,SEO 變成很多網站在意的事,從而產生了內容農場這樣製造垃圾的存在。

到這裡我們先總結 Context Widows 的結論:

LLM 並非科學出版功能失調的始作俑者;它們繼承了這種功能失調,加劇並使其運行得更快。就像一種通常無害的病原體在免疫功能低下的患者體內肆虐一樣,它們指出了問題所在,但如果將它們視為問題的全部,那就大錯特錯了。人們或許會希望這種加速發展能加劇矛盾,讓系統如此迅速地產生大量劣質內容,最終讓問題變得無可辯駁。但是,正如我們現在應該都明白的那樣,系統可以無限期地處於功能失調狀態,而荒謬本身並不會自我糾正。這種加速發展究竟會導致崩潰、適應,還是只是延續現狀,並非技術本身的問題,也無法透過關於技術能力的爭論來解答。答案將由運行這個計畫六十年的機構來解答。

於是現在我們可以更進一步的詢問:LLM 能否有效的融入商業的創新、運作和責任之中?

LLM 能否有效的融入商業的創新、運作和責任之中? [local-1]

同樣的,這使得我們去問:LLM 出現之前,商業運作著怎樣的體系?

在經濟學追尋公司的意義時,Milton Friedman 在 1970 年提出 The Social Responsibility of Business Is to Increase Its Profits,直譯大概就是「企業的社會責任是增加利潤」,這是當代企業的主流追求與試圖實踐的理想。這有什麼樣的後果?

Paul 有兩篇相關的 thread,很清晰的闡述了當代的企業環境

  1. https://hachyderm.io/@inthehands/113603661215753448
  2. https://hachyderm.io/@inthehands/115793003693199462
儘管我們大多數人都生活在現代企業地獄般的環境中,仍然無法理解大企業的運營究竟有多麼嚴重的破碎,以及它們在多大程度上依靠虛假、欺騙和胡言亂語來運作。

所以,為什麼公司部署 LLM 呢?Paul 讓我們從底線來看:XX 公司的新 AI 客服描述不存在的功能,或是叫人們去吃釘子或膠水。會怎樣嗎?嗯反正他們以前那些不幸的、缺乏訓練的、工資微薄、像泥土一樣被對待的客服人員(過去負責處理所有的客戶支援),實際上也沒有幫助人們。由於 XX 公司對吞吐量的要求如此之高,並且對問題關閉的激勵如此之大,以至於他們的客服人員帶領人們白費力氣,罵人或放棄回應。LLM 不會罵人,不會放棄回應,吞吐量極高。它在帶領人們白費力氣上「遠遠」比人類更有效率,而且有時候僅僅靠運氣,它實際上是對的!

從底線來看,部署 LLM 其實沒有那麼糟糕!所以現況其實是

商業活動為世界提供的實際價值如此之少,以至於用快速、自動化的廢話取代緩慢的人類廢話似乎是一場勝利。

所以對於採不採用 LLM,我們可以問的問題是:如果它是錯誤的,那麼重要嗎?Paul 以特斯拉為案例:特斯拉正在銷售這些「顯然」還沒有準備好迎接黃金時段的自毀汽車,它們會用事故後打不開的車門將人們困在裡面,然後將他們活活燒死。而且他們仍然在銷售大量這些東西。如果對他們來說這樣都無關緊要,那有多少虛假和危險的商業環境是可以被他們接受的?

LLM 是假的?又怎樣呢?商業也是如此。

更具體的說,LLM 會對管理層帶來什麼影響?對糟糕的組織領導人來說,讓擁有專業知識的下屬不去檢查他們的想法基本上就是成癮性藥物。那 LLM 豈不是就是不會拒絕他們任何「高妙」想法的完美下屬?這是一種強大的心理力量,促使我們快速地往採用 LLM 滑動,無關乎這是否有用。

所以同樣的,LLM 表現出的,是對既有商業實踐的加劇與加速,放大既有的功能失調。在 LLM 以前同樣有 08 年的次貸風暴,遵循一樣的系統運行規則而致。

監控資本主義提出現代商業的核心邏輯是:資本的來源是販售預測;最容易被預測的人群,是被監控甚或控制的人群。因此獲利的動力會驅動大規模的監視與控制。

按吳修銘的話來說,即使是 Google 最大的反對者也很難說監控資本主義談的是 Google 的主要目的。但這可以被視為一個特別黑暗的隱喻,指向現代商業實踐的最糟結果。而現實中最接近的,是中俄等專制政權已經學會怎麼用現代技術影響他國的政治。

  1. 《讀賣新聞》報導台灣政府正高度警戒中國透過操縱輿論介入選舉
  2. 法國國際廣播電台報導歐洲如何應對俄羅斯的政治宣傳和軍事威脅?

再次的,LLM 提供了加劇破壞社會討論的一種方式,即用無限量的劣質資訊破壞言論場域,但這是因為網路早已逐漸侵蝕了公眾對談的能力。參見 Are we too connected to social media? An explanatory research the problematic social media use in self-esteem, phubbing and procrastinating behaviour。

LLM 能否有效的融入人的成長、交流與生活中? [local-2]

LLM 能否有效的融入人的成長、交流與生活中?這當然是一個複雜的問題:LLM 出現之前,現代人類是怎麼成長、交流與生活的?

  1. 現代的主流成長方式,是未成年人類放到學校;而成年人上網看一些什麼如何投資、如何成長的東西,比如人們會看教你如何閱讀的影片,但不會真的去閱讀。對了,老闆想要成長 — 我們就讓老闆「看見」成長。
  2. 現代的主流社交方式,是在稱之為社群媒體或是社交平台(像是 threads、IG、FB)的地方對幽谷大喊,由演算法控制人們看到什麼(你很好奇為什麼 meta 好像持有前述的所有平台?這是很好的好奇)
  3. 現代的主流過活方式,是必須上 8 小時以上的班,換取大概能過活的薪水,在剩餘的時間想辦法娛樂自己,我們勉強稱之為生活

LLM 會怎麼影響,要看 LLM 有哪些具體的施展位置。

  1. 在學校逐漸提高效率要求的情況下,你要怎麼譴責老師:用 LLM 生成題目、改答案、規劃教學?

  2. 在課業繁重的情況下,學生要怎麼不用 LLM 生成作業?尤其是成績如果能因此提高,而系統獎勵成績時?

  3. 人們要怎麼分辨出 LLM 生成的留言然後避免被影響?

  4. 人們要怎麼在生活壓力越來越大時不對 LLM 宣洩情感?至少它不會罵我們

  5. 人們要怎麼在不因為能對 LLM 下令而被權力損傷?

    oh, I just realized: does this mean that AI gives everyone the chance of the brain damage humans get from too much power too long?

    參見 Power changes how the brain responds to others。

  6. 要怎麼在搜尋引擎預設就提供 LLM 合成答案時去搜尋原始文章,確認內容?

  7. 要怎麼在繁重的工作壓力下保持生活?

上述的問題,有些出自商業邏輯,有些跟我們的社會運作有關,有些是因為人類的生理能力。再次的,LLM 加劇我們的問題,但不是唯一的原因。要讓 LLM 技術不對人類造成危害,就要在這些具體的案例中找出我們的解答。

注意到,這篇文章的目的不是號稱 LLM 全然的不好,或是商業多麽的灰暗;即使是最荒謬的公司中都有很多好人試著改善問題;學術界就算深受引用指標影響,也依然有大量有用的研究出產。最糟糕的莫過於直接跳過具體案例的討論,做出這個世界已經完全沒有救的結論。

這篇文章的目的是,從我有限的觀察與閱讀中,指出具體的商業、科技與政治問題,並且試著改善它們。如果問萬事揭曉:打破文明演進的神話,開啟自由曙光的全新人類史最重要的意義是什麼,我會說是 Graeber 指出人類具有想像並構造自己的政治的能力,遠超過當代人對自己的想像。或許我們可以試著打造一個世界,減少指標的需要、降低對效率的要求、更關心你我周遭的人、更注重環境對我們說的話,這不是什麼烏托邦,衝突不會簡單的消失,但我們需要真的開始對話跟行動,這一切才有出路。

2025 年 [2025]

今年開源的一點貢獻

然後我下定決心研究了 epub 檔案格式,讓我更清楚可以對這些書籍檔案做什麼xd。因為年初我自己立的目標是看懂 mirror symmetry,數學我今年大致上學了

  • mirror symmetry 的表示論前提

    • k\mathbb{k}-linear category
    • cochain complex: 一系列 vector spaces CiC^i(或是 graded vector spaces)加上一個 degree 1 的 linear map dd
    • cohomology: dd 按組成取 Ker dIm d\frac{\text{Ker}\ d}{\text{Im}\ d} 也是一個 graded vector space,這就是 cohomology 了
    • A∞A_\infty algebra 與 category
    • Twisted complex

    另外從微分幾何的方向看了一點物理學的對稱性怎麼用來描述費米跟玻色場。不過這些內容的意義為何?要解決什麼問題?似乎又回到需要代數幾何的基礎上,因此這裡得先放著了。

  • preadditive/abelian category
  • 一點 commutative algebra
  • 一點 Lie algebra

今年末尾嘗試寫了各種 agda,讀了點 HoTT 中的 higher inductive types 跟 synthetic geometry,結合上面需要代數幾何的部分,讓我想往 synthetic geometry 的方向試試,或許先看看幾篇相關論文的未來研究方向跟未解問題?

閱讀 [local-0]

  1. 監控資本主義時代 上卷: 基礎與演進/ 下卷: 機器控制力量:除了這本書本身指出的極端情況,還有數個對現況的相關看法應該一起吸收,我應該會再寫一篇文章寫這方面的感想
  2. 萬事揭曉:打破文明演進的神話,開啟自由曙光的全新人類史:作者認為大眾對人類發展史的看法深受盧梭與霍布斯的遺緒影響,而考古與人類學證據足以駁斥這些觀點
  3. 慈禧: 開啟現代中國的皇太后:這是在 mastodon 上看別人看很有趣就去買的書,作者盡力基於歷史證據描繪了一個與學生時代課本所述完全不同的慈禧
  4. 不實在的現實:以演化為基本的公理的話,我們會推導出什麼樣的世界觀?我覺得有關聯、也很有啟發性的是訪談 Michael Levin 討論的事情
  5. 勤儉魔法師的中古英格蘭生存指南、侑美與夢魘繪師、翠海的雀絲、日煉者:嚴格來說日煉者我只看了一半,不過應該很快就能看完xd
  6. 裸顏(Till we have faces):這本小說的劇情很明顯不能按照字面意思去讀,賽姬跟歐若要解釋成一個人或是兩個人都可以,而「神」的意義也明顯有多重意義,每次閱讀應該都會投射自己的人生感想進去吧xd

技術性的東西其實比較好寫,做到哪裡就說什麼。今年六月的時候去台中找朋友,雖然大罷免沒有成功,但很高興台灣有這麼一群人,民主的重點從來都不是政黨,而是表達意願、付諸行動的公民。台灣的未來、人類的未來,目前看起來都比較灰暗,我不會指望明年就會有什麼差別,但願我們能學會不輕視人文、理解不同的文化、解決共同的難題。

Rhombus 用標記運算自動排序資料 [programming-0005]

最近在研究 Shrubbery Notation 怎麼實現 operator 優先序的過程中(卡關中),又發現 Rhombus 的 meta 功能中有不少有趣的東西,比如現在要介紹的功能:標記運算。要使用這個功能,必須在開頭宣告

#lang rhombus/and_meta

或是

#lang rhombus/static/and_meta

語言。接著定義

annot.macro 'AscendingIntList':
  'converting(fun (ints :: List.of(Int)) :: List: ints.sort())'

這會在我們寫下

[3, 1, 2] :: AscendingIntList

時,得到 [1, 2, 3] 而不是 [3, 1, 2],因為這個標記會自動進行排序運算!

具體的 simplicial set: Torus [math-001J]

simplex 要怎麼跟幾何扯上關係?這是因為 up to homotopy,我們可以把幾何形狀變形成用一群三角形(確切的來說是一些 simplex)表示的形狀。這裡要講的案例 Torus 是一個二維度的幾何曲面,一般定義成 S1×S1S^1 \times S^1 並畫成

要怎麼三角化呢?通常會簡化成

這怎麼變成幾何形狀的?注意到標為同一個名稱的那些線,那表示那其實是同一條線。這需要定義一個 simplicial set S\mathcal{S}(Hint: 根據定義是個 contravariant functor),其資訊如下:

S[0]= {p}S[1]= {l0,l1,l2}S[2]= {a,b}Sδi(lj)= pSδi(a)= liSδi(b)= l2−i\begin{aligned} \mathcal{S}[0] =\ &\{ p \} \\ \mathcal{S}[1] =\ &\{ l0, l1, l2 \} \\ \mathcal{S}[2] =\ &\{ a, b \} \\ \mathcal{S}\delta_i(l_j) =\ &p \\ \mathcal{S}\delta_i(a) =\ &l_i \\ \mathcal{S}\delta_i(b) =\ &l_{2-i} \\ \end{aligned}

視為其幾何實現(geometrical realization):

∣S∣:=(⋃iS[i]×Simpi)/∼|\mathcal{S}| := \big( \bigcup_i \mathcal{S}[i] \times \text{Simp}_i \big) / \sim

每個 topological space X\mathbb{X} 都導出一個 simplicial set X\mathcal{X},每個 kk-simplex 都是 Simpk\text{Simp}_k 到 X\mathbb{X} 的連續函數:

X[i]:= HomTop(Simpk,X)X[δi](f):= f∘δi\begin{aligned} \mathcal{X}[i] :=\ &\text{Hom}_{Top}(\text{Simp}_k, \mathbb{X}) \\ \mathcal{X}[\delta_i](f) :=\ &f \circ \delta_i \end{aligned}

每個連續函數,都給出了連續變形的過程;第二部分則是取 restriction,保持邊界關係。我下面展示如何逐漸變形來解釋其意義:

接著把管子對接

這確實是 Torus 的模型。

Integrating Mathlive into editor.js and Thoughts on Note-Taking Systems [programming-0004]

editor.js is a free, block-style (composable) editor with universal JSON output. Hence, it's a nice foundation for building your own rich-text editor.

mathlive is a math editor for the Web, specifically a custom HTML element with functionality to edit math formulas:

<math-field>
  x^2 + y^2 = 1
</math-field>

editor.js is extensible. Specifically, when creating an editor.js instance, one can provide customized tools to it:

new EditorJS({
  holder: editorContainer,
  autofocus: true,
  tools: {
    header: Header,
    underline: Underline,
    strikethrough: Strikethrough,
    list: List,
  }
})

These tools are node packages you need to install. As you can see, editor.js is highly modular, which is good because I'm going to add a new tool here. I'm going to show how to build a mathlive tool called Formula for editor.js (and perhaps I'll extract it as a package in the future).

A custom tool requires some basic setup, which is common to all tools

class Formula {
  static get toolbox() {
    return {
      title: "Formula",
      icon: "...",
    };
  }

  constructor({ data }) {
    this.data = data;
    this.wrapper = undefined;
  }
}
  1. A tool is a JS class
  2. In toolbox we can configure the title to show and the icon (which is a string of svg syntax)
  3. We need to restore from data when constructing the tool (when loading from existing JSON!)
  4. and we need a wrapper (a HTML element) to store the UI of this tool

Then we need to define how to store data into the output JSON, so we define save method

save(blockContent) {
  return {
    latex: blockContent.value,
  };
}

The blockContent is what the render method returns. We'll look at that part now

render() {
  this.wrapper = document.createElement("math-field");
  this.wrapper.setValue(this.data.latex);

  this.wrapper.macros = {
    RR: "\\mathbb{R}",
  };
  this.wrapper.mathVirtualKeyboardPolicy = "sandboxed";
  this.wrapper.addEventListener("focusin", (evt) =>
    window.mathVirtualKeyboard.show()
  );
  this.wrapper.addEventListener("focusout", (evt) =>
    window.mathVirtualKeyboard.hide()
  );
  this.wrapper.addEventListener("keydown", (e) => {
    const navigationKeys = [
      "ArrowLeft",
      "ArrowRight",
      "ArrowUp",
      "ArrowDown",
      "Home",
      "End",
      "Backspace",
      "Delete",
      "Enter",
      "/",
    ];
    if (navigationKeys.includes(e.key)) {
      e.stopPropagation();
    }
  });

  return this.wrapper;
}

This function is quite long, but let's break it down

  1. We assign the HTML element math-field to the wrapper
  2. Set the value of the math-field to the LaTeX formula loaded from data (which can be undefined)
  3. Define macros for LaTeX input
  4. Configure the policy and focusin/focusout event handlers to automatically open the virtual keyboard when the user focuses on the math-field
  5. Add an event listener to intercept keys that would otherwise cause you to lose focus on the math-field (remember that editor.js is an editor, and those keys would trigger other operations in the background editor)
  6. Finally, we return the wrapper, this HTML element will be the UI element of this tool

At the end, remember you also need to import css of mathlive in HTML

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/mathlive/mathlive-static.css" />

How to package Mathlive JS code depends on your bundler - that's an entirely separate issue that I won't cover here.

Now that the technical details are covered, I can discuss the motivation. Basically, most editors split math formula input into two phases: When you focus on it, it show the original LaTeX syntax so you can edit it. When you focus out, it use KaTeX to render the formula result. But that's messy!

This model makes it easy to miss small problems, that's why there are many small typos in complicated formulas. WYSIWYG is the tool to solve this, and mathlive is such editor I need.

The full plan - though I'm not sure if I'll actually do it - is a complete note-taking system for academic knowledge. Some great tools include forester, Heptabase, Obsidian, etc. However, they all have some rough edges for example

  1. forester has no editor part (there are some external editor projects though)
  2. I can't contribute to codebase of Heptabase
  3. Obsidian's format in long period is problematic
  4. An Obsidian editor extension can't be ported to non-markdown formats, so notes taken here can't be ported to forester, for example

I have to say these aren't problems with the tools themselves, but I do want a tool that has WYSIWYG + portable format (JSON is good enough) + searchability.

Of course, to keep learning progress of mine, I can't spend all the time on a tool - working on problems and reading papers are much more important for learning.

sauron supports find references in project [programming-0003]

sauron now supports find references of a definition in a whole project scope at commit: 3f78957a. This is a final piece of my IDE development for Racket, I had this idea three years ago, but get completed till now. This is because the resource stability of Racket get a huge improved, make this possible, I don't have detailed number, but the same program three years earlier will take down DrRacket.

After the above nonsense, I should quickly explain the idea, it can be break to a few steps

  1. Everytime the project-directory is change (this is a configuration item), send update command to files' maintainer

  2. In each collector, record entries of reference like this: key is filename of definition and identifier of definition, value is the binding location (filename, start, and end position)

  3. When user use Cmd+B/Click, check the cursor is on a definition or on a binding. If cursor is on the later, open list-box to pick a reference/binding, and the selection will bring user to a new location

Now, let's look at the result: a definition in a.rkt, a provide and a local reference/binding, and a reference from another file b.rkt. Three references are showed in the list-box

After click the item, the frame move to binding location at b.rkt.

Anyway, this is a nice end for this project, in the future I will more focus on racket-langserver because it can help more users.

TeXmacs [software-0008]

最近因為朋友提及,所以真正嘗試了 TeXmacs https://www.texmacs.org/ 這個軟體,感受到什麼是所見即所得應有的樣貌,於是想寫下一點介紹。由於 TeXmacs 做的特別好的是數學公式編輯,所以我們集中在這件事上。在 TeXmacs 中輸入 $ 就會立即創造一個 math 區塊,用 Ctrl + $ 則會創造置中的 math 區塊($ 係指 Shift + 4,本文的快捷鍵皆為 MacOS 上的情況,在其他系統需注意差異)。

cursor 的所在區塊會被淺藍色框標記,範例如下

被選擇的區塊,會用紅色標記。用鍵盤或是滑鼠都可以選取

在 math 區塊中輸入分數時,鍵入 \frac 然後按下 Space,就會出現上下兩個區塊

其中紫色的是 cursor,可以用上下鍵切換要在哪個區塊繼續輸入

次方也是同理

對於等號,也有特殊的操作可以使用,比如選擇等號後按下 Ctrl + A,就會在正上方出現輸入區塊

最後,還有一些數學常見的段落,都已經寫成樣板可以直接插入:

插入結果

My exploring Borsuk-Ulam Theorem [math-000U]

My exploration (around April, 2025) of Borsuk-Ulam theorem began with trying to prove the Lyusternik-Shnirel'man closed theorem for S1S^1 and S2S^2. While I could grasp it intuitively, constructing a rigorous proof proved elusive.

This led me to tackle the Borsuk theorem directly: For every continuous function f:Sn→Rnf : S^n \to \mathbb{R}^n, there exists a point x∈Snx \in S^n such that f(x)=f(−x)f(x) = f(-x).

The Failed Stereographic Approach [local-0]

My initial idea was to decompose ff using North/South stereographic projections—the natural atlas for SnS^n. This approach not quite work here: the N/S maps send the projection point to nowhere (or must change codomain to one-point compactification), making any function decomposed this way necessarily discontinuous.

Refinement: Finite Projections [local-1]

I then considered pulling the projection point outward along the N-S axis, forcing projections into finite regions to maintain continuity. This insight—that continuity constraints force us to work in bounded regions—became crucial to my understanding.

Following this idea to examine the N/S map decomposition, after compressing infinity back to Rn\mathbb{R}^n, the continuity requirement also necessitates pulling back the neighborhood, leading to the same conclusion as the outward projection: the region must be finite.

At this moment, I learn the relations between different description of Borsuk-Ulam (Some applications of the Borsuk-Ulam Theorem).

The Breakthrough [local-2]

My proof strategy ended up opposite to the classical development. I first established a lemma:

Lemma: Let f:Sn→Rnf : S^n \to \mathbb{R}^n be continuous and antipode-preserving. Then there exists x∈Snx \in S^n such that f(x)=0f(x) = 0.

The details can be found here, the key is f(E):Sn−1→Sn−1f(E) : S^{n-1} \to S^{n-1} bound an open set of Rn\mathbb{R}^n that contains 00 and is the image of rest two open sets on SnS^n. Hence, f(x)=0f(x) = 0 for some xx

Proof [local-3]

For the main theorem, define g(x)=f(x)−f(−x)g(x) = f(x) - f(-x). Two key observations:

  1. gg is continuous
  2. gg is antipode-preserving: g(−x)=f(−x)−f(x)=−g(x)g(-x) = f(-x) - f(x) = -g(x)

Applying the lemma: there exists xx such that g(x)=0g(x) = 0, which means f(x)−f(−x)=0f(x) - f(-x) = 0, hence f(x)=f(−x)f(x) = f(-x). Proof complete.

The "wrong" approach turns out is useful in lemma, and provides a more concrete view about the problem. After this, I also found Borsuk-Ulam Implies Brouwer: A Direct Construction, which is the same approach, and use cube version.

Determinant 的意義 [math-000Q]

從矩陣非常難看出 determinant 的意義,但它其實具備幾何的意涵:對 nn 個向量張出的高維平行體的體積影響。

矩陣 AA 可以視為張量 V⊗V∗V \otimes V^*,向量張出的高維平行體可以表示為 exterior product v1∧⋯∧vnv_1 \wedge \dots \wedge v_n。因此定義 det⁡A\det A 如下:

det⁡A(v1∧⋯∧vn)=Av1∧⋯∧Avn\det A (v_1 \wedge \dots \wedge v_n) = A v_1 \wedge \dots \wedge A v_n

這表示 AA 把 viv_i 映射到 AviA v_i 所得的新高維平行體的體積相對於先前的高維平行體的比例(可能會正負轉換)。

補充:當 dim⁡V=N\dim V = N 時。NN 階 skew-symmetric tensor 的空間 ΛNV\Lambda^N V 很明顯只有一個維度,這個空間在 Lectures on the Geometry of Manifolds 中叫做 Determinant line of VV。此時 ΛN+1V\Lambda^{N+1} V 全都是 trivial space(只有 00 元素)。

部落格遷移 [blog-0000]

最近終於自己寫了個 site generator tr,這套機制確實是解決我很多問題,所以就開始遷移舊的部落格了。原則上大部分文章或是筆記都不會跟過來,因為這也是趁機丟掉一些已經過時/太簡單的筆記的機會,tr 讓我可以直接嵌入 wiki/nLab 來取代解釋基礎的定義。

NixOS 需要定期清除 generations [software-0000]

這是今天遇到的問題,更新系統時它提示 /boot/ 的空間不足以安裝 efi 檔案了,直接砍 /boot/EFI/nixos/ 裡面的檔案是沒有用的,因為 NixOS 的 generations 會重新生成這些資料。所以正確的作法是先執行下面的指令

cd /nix/var/nix/profiles

裡面會有很多像是 system-數字-link 這樣的 soft links,每一個對應一個 NixOS 的 generation。把太古老的 generations 刪掉,執行

nix store gc

這會跑一段時間(如果你跟我一樣太久沒有清理就需要很久),結束之後再次更新系統就可以了。

何謂 elaboration? [cs-0004]

elaboration 指的是轉換沒有語意的表面語法(surface syntax 或是說 source code)到有語意的語言表示(language representation)的過程,這樣講可能有點抽象,所以需要一些例子。試問 foo(a, b) 這段 Go source code 有語意嗎?事實上是沒有,因為只有在 Go compiler 經過

  1. 讀取文字
  2. 解析語法取得語法樹
  3. 型別以及其他檢查

現在這段程式碼才會在 Go compiler 內部用某種資料結構表示,而 Go compiler 可以相信這段程式碼是滿足 Go 規範的程式,這樣才是有語意的語言。這個觀點一開始對你來說可能蠻怪異的,但我們可以仔細來看這樣定義的好處

  1. 有些語言如 Fennel https://fennel-lang.org/ 刻意把自己變成另一個語言 Lua 的另類文法,在語言表示層面要求共用一樣的語意。Fennel 還支援了 macro 系統
  2. 語法可以作為片段,嵌入到任何位置,只不過後續的檢查階段可能拒絕嵌入後產生的語法,因為這樣產生的程式依然可能是語法或是語意不正確的,比如使用 C 語言的 preprocessor 寫程式就經常會遇到這樣的問題。能把語法記錄下來並在別的地方展開的程式就叫做 macro,LISP 的 elaboration 經常叫做 expander

Macro system [cs-0005]

雖然 C 的 preprocessor 也被稱為 macro,但跟 LISP、Elixir、Julia 比起來就差很多的原因是

  1. C 對語法的原始位置放棄追蹤
  2. C 對 macro 的輸入跟輸出都沒有基本的檢查,也沒辦法對不同輸入做出反應

而常見的 LISP macro 過程則是:

  1. reader: TEXT -> S-expression
  2. 使用者定義的 macro: S-expression -> S-expression
  3. expander: S-expression -> LISP

elaboration 前對語法樹進行操作的能力就是 LISP 最大的特徵,這使語言使用者可以自行發展他想要的語法,Clojure 做得比傳統 LISP 更好的地方就是這裡它採用的 edn 能表示比 S-expression 更多的資料,如 hashmap。

什麼是 subroutine? [cs-0006]

subroutine 在 C 語言被稱為 function,Fortran 中則分出 function 或是 subroutine,剩下還有一些語言使用 procedure 這個名稱。

Fortran offers two different procedures: function and subroutine. Subroutines are more general and offer the possibility to return multiple values whereas functions only return one value. https://fortranwiki.org/fortran/show/procedure

我把定義 subroutine 為一個指令語言編寫程式的模式,在組合語言裡面,我們並沒有函數名稱這種東西,相對的,其實只有指令的位址,所以組譯語言通常會提供區塊名稱來標記位址,比如 a:,那你可能已經想到了,如果我跳轉到 jump a 是不是就能執行 a 的內容了!是,但你要怎麼跳回來呢?所以 subroutine 就被發明了:找個 register 放一下要跳轉回來的位址就好啦!於是一個 a 呼叫 b 的程式就寫成

a:
  store ret (here+1)
  jump b
  ...
b:
  ...
  jump ret

(here+1) 是為了跳過 jump b 這個指令,而 ret 就是一個特殊保留的 register,專門用來讓 subroutine 使用。你會發現,ㄟ這樣是不是沒有傳遞參數?沒錯,所以更完整的故事是,CPU 會乾脆設計一整套傳遞參數應該用哪幾個 registers 跟 stack 偏移的慣例,叫做 calling convention。

這就是所謂的現代 CPU 是為了 compiler 設計。不遵守這些慣例你的程式還是能動,但你的程式就會跟其他 compiler 為這個 CPU 吐出來的程式很難溝通。

作業系統的行程排程 [cs-0007]

在一些非常簡單的作業系統實現裡面,會乾脆讓 user process 自己決定要不要讓出 CPU,怎麼讓?就是把自己當前的 stack pointer address 以及 registers 狀態儲存到記憶體裡面去,通常是 kernel 提供的一個 struct,然後跳轉到另一個 process 去。

這樣的話這是 user process 自己決定讓給誰,一般其實也不是這樣實現的,而是 user process 只有呼叫 yield 把自己暫停,下一步轉移給誰交給 kernel 決定。比如 Linux 就有

int sched_yield(void)

這個函數。

但要是 user process 不小心陷入無限迴圈怎麼辦?所以更常出現的是所謂的搶佔式排程,kernel 分配 time slice 並且封裝 process,並且設定計時器硬體中斷來確保自己可以定時查看狀態,這樣 process 在 slice 用光之後(或是它的指令執行完畢)就可以被切斷。

Transformation induced dual basis [math-000D]

Manifolds and Differential Geometry

Let e1,e2,…,en∈Ve_1, e_2, \dots, e_n \in V be a basis of vector space VV, and let e1,…,en∈V∗e^1, \dots, e^n \in V^* be a basis of dual space V∗V^*. Now if eˉi=C  ikek\bar{e}_i = C^k_{\ \ i} e_k is another basis of VV, then there is an induced basis eˉi=(C−1)  kiek\bar{e}^i = (C^{-1})^i_{\ \ k} e^k for dual space V∗V^*.

figure tex17814

Proof [local-0]

By Kronecker-delta 1=δ ii1 = \delta^i_{\ i}

1=δ ii=eˉieˉi=C  ikekeˉi1 = \delta^{i}_{\ i} = \bar{e}_{i} \bar{e}^{i} = C^k_{\ \ i} e_{k} \bar{e}^{i}

can see that if eˉi=ek(C−1)ki\bar{e}^{i} = e^{k} (C^{-1})^i_k then the equality is hold. We can use Penrose notation to show the idea.

figure tex17815

用 effect handler 實作 parser combinator [VSMS]

這些程式碼都是使用 OCaml 實現,並使用了 algeff 與 asai 程式庫

解析器需要完成的任務就是根據規則把一系列 tokens 變成文法樹,並且回報為何解析失敗,一般大學作業等級的編譯器都會讓學生直接使用解析器生成器,如 menhir 這種工具。然而實際上開發真實語言的解析器時,往往會遇到需要錯誤恢復並繼續解析,最後再回報多個錯誤的要求,這時候生成器往往給出很差的結果,甚至乾脆就是做不到的。

然而,手工編寫的解析器通常可維護性相當差劣。一種折衷的方案是所謂的解析器組合子,我過去也曾經介紹過如何利用組合子抽象掉重複的解析規則。但這種方案有個問題,就是回溯必須手動使用 try 組合子來在失敗時恢復狀態,但實務上這創造了相當難以理解的各種 try 安插,當我意識到這是因為 API 需要是兩階段的問題時,我就發現其實 effect handler 正是解決這個問題的好方法。

我本來是直接使用 Effect.Deep 的,但後來發現這種情境直接用 algeff 就足夠了,因此改採這個方案。實際程式碼如下

module Tokens = struct
  type t = Lexer.token Asai.Range.located list
end
module TokenState = Algaeff.State.Make (Tokens)

首先,我假設了 Lexer.token 這個型別存在,並且使用者會輸入一個標記過位置的 token list。接著建立一個狀態模組,這個模組的只有三個用法:

let next_token () =
  match TokenState.get () with
  | [ eof ] -> eof
  | tok :: buf ->
      TokenState.set buf;
      tok
  | [] -> raise Impossible

let shift pos = TokenState.set pos
let current_position () = TokenState.get ()
這段程式巧妙的使用了 OCaml 的不可變 list 的特性,也就是持頭就可以保證資料不被回收

並且提供啟動函數

let run (init : Lexer.token Asai.Range.located list) (f : unit -> 'a) : 'a =
  TokenState.run ~init @@ fun () -> f ()

輔助函數 Asai 的例外捕捉 [local-0]

let catch_parse_error (p : unit -> 'a) : 'a option =
  let pos = current_position () in
  Reporter.try_with
    ~fatal:(fun d ->
      match d.message with
      | Parse_error ->
          shift pos;
          None
      | _ -> Reporter.fatal_diagnostic d)
    (fun () -> Some (p ()))

其中 Parse_error 跟 Reporter 都是使用者自訂的錯誤回報模組內容,如何定義可以參考 asai 的文件。這段輔助函數之所以出現只是為了讓讀者知道這個函數的存在,這個函數只捕捉解析失敗,讓其他錯誤繼續往上走。

常見的組合子 [OQKM]

直接消耗一個符合預測的 token consume [local-0]

let consume (predict : Lexer.token) : unit =
  let tok = next_token () in
  if tok.value == predict then ()
  else
    (* raise ... *)

重複解析到失敗為止 many [local-1]

let rec many (p : unit -> 'a) () : 'a list =
  let x = catch_parse_error p in
  match x with
  | None -> []
  | Some x -> x :: many p ()

若失敗就解析第二個規則 [local-2]

let ( <|> ) (p1 : unit -> 'a) (p2 : unit -> 'a) () : 'a =
  match catch_parse_error p1 with
  | None -> p2 ()
  | Some x -> x

此外也有更複雜的錯誤恢復機制

如果最上層的定義解析失敗就紀錄失敗訊息並跳到下一個 start token 繼續解析 [local-1]

let rec program () =
  let x = catch_parse_error top in
  match x with
  | Some x -> x :: program ()
  | None ->
      let pos = next_start_token () in
      shift pos;
      program ()

Algorithm Elaboration zoo 中的合一演算法與隱式參數 [tt-0006]

程式語言為了讓使用者省略不必要的輸入,必須為使用者推導內容,這種需求催生了合一這種類型的演算法。比如很多程式語言允許泛型,比如下面的 OCaml 程式碼

let id (x : 'a) : 'a = x

Elaboration zoo 這個教學專案,它的程式碼要解決的,是一類更複雜的程式語言,叫做 dependent type 的系統中的合一問題,這類系統最重要的特性,就是類型也只是一種值,值也因此可以出現在類型中,但這就比較不是本篇的重點,有興趣的讀者可以閱讀 The Little Typer 來入門。在解釋完它的合一之後,我會介紹它隱式參數的解決方案

Algorithm 合一演算法的過程 [tt-0007]

假設有程式碼

let id {A : U} (x : A) : A := x
let id2 {B : U} (x : B) : B := id _ x

這裡 id _ x 中的底線表示「省略的引數」,被稱為 hole,我們並沒有明確的給它任何值,比如 B。在展開這個 hole 時,我們用 meta variable ?0 替換它。但 meta variable 必須考慮 contextual variables 的 rigid form,因此進行套用得到 ?0 B x。這引發問題

什麼是 contextual variables?為什麼 contextual variables 是 B 與 x? [tt-0009]

答案是,這些是 ?0 能看到且實際內容未知的變數,只能用 rigid form 替代

什麼是 rigid form? [tt-000A]

rigid form 是 dependent type 中即使是 open term 也必須先進行計算而被迫在實作中出現的一種值;由於 meta variable 而被迫出現的值則叫做 flex form

現在我們手上有的 term 是 id (?0 B x) x,現在執行 id (?0 B x),我們得到型別為

(x:(?0 B x))→(?0 B x)(x : (?0\ B\ \textcolor{red}{x})) \to (?0\ B\ \textcolor{red}{x})

的 term

λx.x\lambda x. x
注意到 (x : (?0 B x))(x\ :\ (?0\ B\ \textcolor{red}{x})) 中兩個 xx 來源不同,不可混用,我用顏色表示它們的 scope 其實不同這件事

現在進入到把這個 term 套用到 x 上的環節了,因此觸發 x : Bx\ :\ B 是否是 ?0 B x?0\ B\ x 的 term 的合一計算,我們概念上紀錄成

?0 B x=?B?0\ B\ x \stackrel{?}{=} B

現在合一演算法必須找出合適的 ?0?0 來滿足等式。我們知道 ?0?0 應該是某種 lambda

λ1.λ2.?\lambda 1. \lambda 2. \fbox{?}

,顯然 ?=1\fbox{?} = 1 就會給出我們想要的答案,但這部分要怎麼做到?elaboration zoo 的演算法分成三個階段

Invert [tt-000B]

這裡的重點是,既然 ?0?0 已經套用了 BB 與 xx,因此就創立關聯它所創立的 lambda λ1.λ2.?\lambda 1. \lambda 2. \fbox{?} 中的變數到其 contextual variables 的映射(mappings)是

1↦B2↦x1 \mapsto B \\ 2 \mapsto x

現在右手邊的 term 只需要看自己的每個 rigid form 有沒有被指向,只要這個映射裡面查找到有變數是指向 rigid form 自己,即反過來創立 rigid form 指向新名字的映射。比如說我們右手邊的 term 是一個 rigid form BB,因此得到的反映射是

B↦1B \mapsto 1

Rename [tt-000C]

在上一步中得到的反映射是

B↦1B \mapsto 1

用這個映射去改寫右手邊 term BB 的內容,就得到等式 ?=?1\fbox{?} \stackrel{?}{=} 1,而解則是 ?=1\fbox{?} = 1。

Lambda 化 [tt-000D]

最後真正構造一個 lambda abstraction,就得到了

λ1.λ2.1\lambda 1. \lambda 2. 1

安插隱式參數的方法 [tt-0008]

在最開始的程式碼

let id {A : U} (x : A) : A := x
let id2 {B : U} (x : B) : B := id _ x

裡面,其實 {} 中的綁定叫做隱式參數,這表示我們其實也可以用 id x 來調用該函數。但這時候要怎麼知道需要補一個 hole 進去呢?elaboration zoo 作者提出的簡易方案就是區分 implicit Π\Pi type 與 implicit application,如此一來,當一個 implicit Π\Pi type 被 explicit apply 的時候,我們就知道要安插 hole 了

以 id x 而言,如果要明確的用 implicit 調用它,就必須寫成 id {B} x,在語法就進行明確的區分。

這時候跳回去看「contextual variables 是 ?0?0 能看到且實際內容未知的變數」就更能了解其理由了,因為已經能明確知道其內容的變數,如

let x = t

x 就是指向 t 的情況,若右手 term 是 x,其實也會是 t 去進行 unify。若右手 term 已經是 rigid form 又可參照,就更不需要成為 contextual variables,因為其 solution 就是把右手 term 原封不動放入,在反轉 map 的時候,只要記得這個 rigid 是非 contextual,就可以為它寫一個 identity mapping。

相關:higher-order-unification/explanation.md at master · jozefg/higher-order-unification --- github.com

racket 中的有界續延如何實作 effect system 的功能? [cs-000G]

一個 effect system 大概做了什麼,我們從 effekt 語言 (https://effekt-lang.org/) 這個案例著手

  • 一個簽名 effect yield(v : Int) : Bool
  • 一個 handler try { ... } with yield { n => ...; resume(n < 10) }
  • 在 try 區塊中調用的函數 f 可以用 do yield(k) 來調用 handler
  • 當 f 執行到 do yield(k),就會跳轉到 with yield 中繼續進行,並且 n = k
  • 當程式執行到 resume 就會跳回去 f 之後的 do yield(k) 的續延繼續執行

因此 exception 系統也可以被視為,沒有調用 resume 的 effect handler,讀者或許也想出數種使用方法了吧?這正是 effect system 的價值。而 racket 擁有比 effect handler 更複雜的機制(continuation with marks),不過 effect handler 機制具有保障使用者不容易出錯的好處,因此在 racket 中模擬一套 effect system 依然有意義,也是有趣的挑戰。

call/prompt [rkt-0000]

call/prompt 的主要功能,就如其名稱,是為被呼叫的函數提示一個標記,這個標記在 abort/cc 會被使用。完整的 call/prompt 調用是

(call/prompt f tag handler args ...)

f 是被調用的函數,中間的 tag 是標記,handler 是處理標記的函數,剩下的都是 f 的參數。讀者可以想像成

(with-handler [tag handler]
  (f args ...))

abort/cc [rkt-0001]

abort/cc 的調用是

(abort/cc tag vs ...)

tag 當然就是先前 prompt 寫進去的 tag,而 vs ... 則作為呼叫 handler 的引數

(handler vs ...)

Continuation prompt tag 的產生方式 [rkt-0002]

調用 (make-continuation-prompt-tag) 產出,這個函數還可以接受一個 symbol 產生可讀的識別碼,並且每次調用這個函數,產生的 tag 都算是不同的 tag,勿必要儲存好這個實體。

要想實現 exception 目前的程式已經完全充分了,只要寫下

(define (f params ...)
  (abort/cc tag "cannot read file ..."))
(call/prompt f
             tag
             (λ (err)
               (printf "got err ~a~n" err))
             args ...)

即可。事實上 racket 自身的的 exception 也是這樣實作的,要考慮的問題是,前面我們看過 resume 可以跳回去被呼叫的函數!這是如何做到的?

call/cc [rkt-0003]

call/cc 這個函數是 call with current continuation 的意思,比如說在下面的程式碼中

(* 2 1)

1 的 continuation 就是把 1 挖掉留下的 (* 2 hole),故下列程式碼

(* 2 (call/cc (λ (k) (k 1))))

的結果是 2。讀者可以想像 k = λ (hole) (* 2 hole),這在實作上不太正確,但對理解很有幫助。

一般來說,racket 的函數會簡單的寫成

(define (f params ...)
  stmt1
  ...
  stmtn)

如果我在其中安放一個 call/cc 會發生什麼事呢?

(define (f params ...)
  ...
  (call/cc (λ (k) ...))
  stmtk
  ...)

答案是 k = λ () (begin stmtk ... stmtn)。由於 racket 自動把

stmt1 ... stmtn

包裝成 (begin stmt1 ... stmtn),而

(begin stmt1 stmt2 ...)

中 stmt1 的 continuation 是 (begin stmt2 ...),以此類推就很容易理解 k 等於什麼了。

我們正是需要使用 call/cc 來做出 resume 的效果。

實現 resume [cs-000H]

在 f 中我們寫下

(define (f params ...)
  (call/cc (λ (k) (abort/cc tag k ...))))

在 call/prompt 的 handler 中新增一個 resume 參數,這樣就完成了。讀者可以填補下面的程式中的空白來檢驗結果,也充當練習

(define (f params ...)
  (call/cc (λ (k) (abort/cc tag k ...)))
  ...)
(call/prompt f
             tag
             (λ (resume ...)
               ...
               ; 跳回 f 繼續
               (resume))
             args ...)

到這裡我們已經有了運行的基礎,要改善有幾個方向可以參考

  1. 用 macro 包裝語法
  2. 放到 typed/racket 中加上 effect 類型檢查確保使用者沒有漏掉 handler:effect system in typed/racket
  3. 支持 finally handler,保證任何退出行為都會被這個 handler 攔截(考慮 racket 的函數 dynamic-wind)

使用 TLA+ 的第一步 [tla-0000]

由於很多 TLA+ 入門教學似乎都沒有談到實務上要從哪裡開始使用 TLA+,所以這裡我想介紹實際上要怎麼做。一個 TLA+ 的檔案以 .tla 為副檔名,其中由 --- MODULE name --- 開頭,由 === 結尾,例如

---- MODULE plustwo ----
=====

在其中可以引用模組,寫成 EXTENDS xxx, yyy, ...

---- MODULE plustwo ----
EXTENDS Integers
=====

使用 PlusCal 語言 [tla-0001]

比起直接寫出 Spec 等不變式,先寫 PlusCal language (https://lamport.azurewebsites.net/tla/tutorial/intro.html) 的程式再使用用它生成的 Spec 不變量會更簡單也更實用。因為 TLA+ 會嘗試生成所有可能的狀態,有可能出現所謂的 unbound model (https://learntla.com/topics/unbound-models.html),就結果而言對它進行檢查永遠不會完成。用 PlusCal 語言寫的程式相對容易發現這種錯誤。

---- MODULE plustwo ----
EXTENDS Integers

CONSTANT Limit

(* --algorithm plustwo
variables x = 0;
begin
    while x < Limit do
        x := x + 2;
    end while;
end algorithm; *)
\* BEGIN TRANSLATION (chksum(pcal) = "3f62e9ff" /\ chksum(tla) = "239a9ecd")
\* END TRANSLATION
=====

由 PlusCal 產出的不變量會被包在 BEGIN TRANSLATION 到 END TRANSLATION 裡面,可以發現它還會檢查 checksum。由於這個區塊內的程式會被機器生成的內容覆蓋,因此千萬不要把自己寫的不變量放在這個區塊中。

上面的 CONSTANT Limit 可以在 model 的設定檔 .cfg 中指定,這是為了避免把常數寫死在 TLA+ 主程式中,好在檢查前可以進行修改。

增加不變量 [tla-0002]

在有了這些程式之後,可以開始增加自己的不變量

Even(x) == x % 2 = 0

IsInc == x >= 0

Inv == IsInc /\ Even(x)
THEOREM Terminated == Spec => Termination
  1. IsInc 要求 x 總是大於初始值 0
  2. Even(x) 要求我們寫的程式只會產生偶數
  3. Inv 把所有我們想證明的不變量組合在一起,/\ 表示邏輯的「a 且 b」語句。我想很容易猜到 \/ 表示邏輯的「a 或 b」語句

THEOREM 部分宣告 Spec 蘊含 Termination 不變量,這兩者都是 PlusCal 產生的,所以不用太在意細節,這裡的意義是證明程式確實會終止。

這裡介紹了 TLA+ 如何運作,TLA+ 對現實程式的幫助會體現在編寫非同步的程式時,TLA+ 擅長對只有有限狀態、可以使用經典邏輯推導的程式進行檢查,主要可以發現死鎖與違反不變式等錯誤。對 TLA+ 所使用的數學方面的學習可以參考 Lamport 本人寫的 A Science of Concurrent Programs。

The concept of webmention [webmention-0000]

Each web page should provide the following content (use my site dannypsnl.me as example) in

<link href="https://dannypsnl.me/webmention-endpoint" rel="webmention" />

sender view [webmention-0001]

According to webmention spec, if someone tries to notify my site that there is a link mentions my post, she has to do the following:

  1. curl -I https://dannypsnl.me/xxx to get my webmention endpoint in the head
  2. post the data, where is the link refers to my post
    POST /webmention-endpoint HTTP/1.1
    Host: dannypsnl.me
    Content-Type: application/x-www-form-urlencoded
    
    source=
    target=https://dannypsnl.me/xxx

receiver view [webmention-0002]

Therefore, a receiver is a POST handler. According to spec, we can

  1. return http code 201 with Location in header pointing to the status URL
  2. return http code 202 and asynchronously perform the verification
  3. return http code 200 and synchronously perform the verification (not recommended), by section 3.2.2
    Webmention verification should be handled asynchronously to prevent DoS (Denial of Service) attacks.

Verification [local-0]

  • The receiver must check that source and target are valid URLs [URL] and are of schemes that are supported by the receiver. (Most commonly this means checking that the source and target schemes are http or https).
  • The receiver must reject the request if the source URL is the same as the target URL.
  • The receiver should check that target is a valid resource for which it can accept Webmentions. This check should happen synchronously to reject invalid Webmentions before more in-depth verification begins. What a "valid resource" means is up to the receiver. For example, some receivers may accept Webmentions for multiple domains, others may accept Webmentions for only the same domain the endpoint is on.
If the receiver is going to use the Webmention in some way, (displaying it as a comment on a post, incrementing a like counter, notifying the author of a post), then it must perform an HTTP GET request on source, following any HTTP redirects (and should limit the number of redirects it follows) to confirm that it actually mentions the target. The receiver should include an HTTP Accept header indicating its preference of content types that are acceptable.

Error response [local-1]

If the Webmention was not successful because of something the sender did, it must return a 400 Bad Request status code and may include a description of the error in the response body.

However, hosting a receiver can be annoying, so we should build on the top of some existed tools.

無外部空間下如何考慮幾何空間的測量? [math-0001]

metric 就是在測量長度。不過在歐式空間裡面討論嵌入其中的曲面時,曲面有外部空間切向量,也就是歐式空間的向量可以使用。如果曲面沒有外空間,就需要自己做一個出來,這就引出了 Riemann metric 的想法:對可微分流形 MM 的每一點 pp 考慮抽象的切空間 TpMT_pM(不考慮 TpMT_pM 到底是什麼),重點是隨點附上內積函數 g:TpM×TpM→Rg : T_pM \times T_pM \to R,因為是模仿內積所以有 bilinearity, positive-definiteness, symmetry 等性質

現在考慮曲線 y:[0,1]→My : [0, 1] \to M,在每一點度量 TpMT_pM 向量並積分(加起來)即是曲線長度。又隨著區間 t:[0,1]t : [0, 1] 改變,會有對應變化的 pp 點,因此模仿並定義

length(y)=∫01g(dydt,dydt)dt\text{length}(y) = \int_0^1 \sqrt{g(\frac{dy}{dt}, \frac{dy}{dt})} dt

g(dydt,dydt)\sqrt{g(\frac{dy}{dt}, \frac{dy}{dt})} 是 norm 的定義,積分因為是跟著 tt 變化,因此依賴 yy 的參數 tt 來定義。如此一來就可以問下一個問題:根據 gg 是從 aa 到 bb 的最短曲線是誰,這就是曲面上的直線的概念(然而,全域與局部最短線,是不一樣的問題,考慮整體將會複雜許多)。

https://qi.bookwar.info/pi-day-special-manifold 對 Manifold 這個想法的高層次概念進行介紹。

為什麼需要 domain theory?遞迴的數學表示 [math-28UX]

一般的 OCaml 函數可以寫成

let f x y z = ...

一個直覺的想法是:每個型別都解釋成一個可數集合,該型別的 term 解釋成這個集合的元素。但有些函數會用到自己本身,例如整數階乘函數

let rec fac n =
  if n = 0
  then 1
  else n * fac(n-1)

它的值是什麼呢?答案是

μ(x:int→int).λ(n:int).{1(n=0)n×x(n−1)(n≠0)\mu (x : int \to int). \lambda (n : int). \begin{aligned} \begin{cases} 1 &\quad &(n = 0) \\ n \times x(n-1) &\quad &(n \ne 0) \end{cases} \end{aligned}

μ\mu 中的 xx 就是 fac 自己,把原始 OCaml 程式中的 fac 換成 xx 即可。問題是集合論沒有辦法充分解釋這個運算,具體來說,集合解釋不滿足 PCF 的 Adequacy theorem。

Theorem Adequacy (充分性定理) [math-CFN7]

If MM is closed term of ground type and C⟦M⟧=C⟦V⟧\mathcal{C}\llbracket M \rrbracket = \mathcal{C}\llbracket V \rrbracket for a value VV, then M⇓VM \Downarrow V.

對一個形式系統來說,需要證明一個假定的 model 確實能表現系統的特徵,所以才需要證明這個定理。參考 computational adequacy

一般來說,只考慮構造與操作規則的話,可以記成下面這樣

Definition Rules of μ\mu [math-TMY9]

Typing [local-0]

Γ,x:t⊢M:tΓ⊢μ(x:t).M:t\frac{ \Gamma, x : t \vdash M : t }{ \Gamma \vdash \mu (x : t) . M : t }

Operational (Big step) [local-1]

[μ(x:t).M/x]M⇓Vμ(x:t).M⇓V\frac{ [\mu(x:t).M/x] M \Downarrow V }{ \mu(x:t).M \Downarrow V }

但我們想要知道在數學上可以用什麼物件表示運算子 μ\mu,也就是指稱語意 (denotational semantic),我先定義一個這個目標需要達成的等式。

基本的約束 [math-2C9J]

解釋 (interpretation)

⟦Γ▹μ(x:t).M:t⟧ρ\llbracket \Gamma \triangleright \mu(x:t). M : t \rrbracket\rho

必須是一個能滿足下面等式的 ⟦t⟧\llbracket t \rrbracket 元素 dd

d=⟦Γ,x:t▹M:t⟧(ρ[x↦d])d = \llbracket \Gamma, x:t \triangleright M : t \rrbracket(\rho[x \mapsto d])
註:ρ\rho 在後面講到語意解釋時會定義

問題是我們怎麼知道存在這麼一個元素呢?由於不動點定理,使得我們有動機把 ⟦t⟧\llbracket t \rrbracket 解釋成 cpo,把計算解釋成 monotone function 再套用不動點定理。從而得到一個合理的定義:least fixed point 即是 μ\mu 的表示物件。

Definition C⟦e⟧\mathcal{C}\llbracket e \rrbracket 的解釋 [math-SSSU]

Notation [local-0]

這裡採用 x↦vx \mapsto v 表示數學中的函數,xx 是輸入 vv 是輸出;但 ρ[x↦d]\rho[x \mapsto d] 則是指 ρ\rho 中的 xx 被替換成 dd

在開始之前我們需要大概了解 C⟦x⟧\mathcal{C}\llbracket x \rrbracket 這個解釋的定義。

  • 當 tt 是型別,則 C⟦t⟧\mathcal{C}\llbracket t \rrbracket 是一個 cpo
  • 當 s,ts, t 是型別,則 C⟦s→t⟧\mathcal{C}\llbracket s \to t \rrbracket 是一個連續函數,domain 是 C⟦s⟧\mathcal{C}\llbracket s \rrbracket 而 codomain 是 C⟦t⟧\mathcal{C}\llbracket t \rrbracket
  • ρ\rho 是一個 Γ\Gamma-環境,幫每個 Γ\Gamma 中的變數 xx 定義一個 ρ(x)∈C⟦Γ(x)⟧\rho(x) \in \mathcal{C}\llbracket \Gamma(x) \rrbracket,Γ(x)\Gamma(x) 是一個型別
  • C⟦Γ▹M:t⟧ρ∈C⟦t⟧\mathcal{C}\llbracket \Gamma \triangleright M : t \rrbracket\rho \in \mathcal{C}\llbracket t \rrbracket,要解釋成三種情形
    • C⟦Γ▹x:t⟧ρ=ρ(x)\mathcal{C}\llbracket \Gamma \triangleright x : t \rrbracket\rho = \rho(x)

      需要 ρ\rho 函數的部分,當我們在程式語言中寫下 let f : T = M 時,就在 ρ\rho 這個部分函數中加入了 f=Mf = M 的定義,注意到 ρ\rho 是部分函數,因為也可能被問到未綁定的變數 zz,這時候 ρ(z)=⊥\rho(z) = \bot

    • C⟦Γ▹λ(x:s).M:s→t⟧ρ=(d↦C⟦Γ,x:s▹M:t⟧(ρ[x↦d]))\mathcal{C}\llbracket \Gamma \triangleright \lambda (x : s). M : s \to t \rrbracket\rho = (d \mapsto \mathcal{C}\llbracket \Gamma, x : s \triangleright M : t \rrbracket(\rho[x \mapsto d]))

      為程式函數找一個數學函數作為解釋

    • C⟦Γ▹M(N):t⟧ρ=(C⟦Γ▹M:s→t⟧ρ)(C⟦Γ▹N:s⟧ρ)\mathcal{C}\llbracket \Gamma \triangleright M(N) : t \rrbracket\rho = (\mathcal{C}\llbracket \Gamma \triangleright M : s \to t \rrbracket\rho)(\mathcal{C}\llbracket \Gamma \triangleright N : s \rrbracket\rho)

      直接調用作為解釋的數學函數

Definition μ\mu 的解釋結果 [math-IG9K]

μ\mu 本身的公式倒沒有很複雜

C⟦Γ▹μ(x:t).M:t⟧ρ=fix(f)\mathcal{C}\llbracket \Gamma \triangleright \mu(x:t). M : t \rrbracket\rho = \text{fix}(f)

其中數學函數 ff 是

d↦C⟦Γ,x:t▹M:t⟧ρ[x↦d]d \mapsto \mathcal{C}\llbracket \Gamma, x : t \triangleright M : t \rrbracket \rho[x \mapsto d]

但我們怎麼確認 fix(f)\text{fix}(f) 的存在?我們已經知道 C⟦t⟧\mathcal{C}\llbracket t \rrbracket 是 cpo。根據不動點定理我們知道只要再證明 ff 是連續函數即可;接著根據下面的定理我們可以知道這能夠套用到任意來自 ground type 的 functional 上

Proposition (cpo)-continuous function space is a cpo [math-LVSY]

If DD and EE are cpo, then the continuous function space

[D→E]={f:D→E∣f is continuous} [D \to E] = \{ f : D \to E \mid f \ \text{is continuous} \}

is a cpo under the pointwise order.

再來就可以選擇 least fixed point 作為 fix(f)\text{fix}(f) 的解釋

⨆n∈ωdn\bigsqcup_{n \in \omega}d_n

其中

  • d0=⊥⟦t⟧d_0 = \bot_{\llbracket t \rrbracket}
  • dn=C⟦Γ,x:t▹M:t⟧ρ[x↦dn−1]d_n = \mathcal{C}\llbracket \Gamma, x : t \triangleright M : t \rrbracket\rho[x \mapsto d_{n-1}]

不過這條鏈 dnd_n 不需要是唯一的,只要對任何一條都能這樣推理即可。

要證明函數 d↦C⟦Γ,x:t▹M:t⟧ρ[x↦d]d \mapsto \mathcal{C}\llbracket \Gamma, x : t \triangleright M : t \rrbracket\rho[x \mapsto d] 對所有 Γ\Gamma-環境 ρ\rho 都成立有點麻煩,需要歸納所有的語法構造,所以這裡就跳過。或許哪天我會寫這部分,不過有興趣的讀者可以先參考 Gunter 的書的第四章,或是自己根據 PCF 這個 metalanguage 進行證明。Sterling 的演講 Synthetic Domains in the 21st Century 也是很好的資源。

Soundness and completeness [math-0000]

  1. Classical logic's soundness says, if ⊢clα\vdash_{cl} \alpha, then α\alpha is classically valid.
  2. Classical logic's completeness says, if α\alpha is classically valid, then ⊢clα\vdash_{cl} \alpha.

可以看到,要用 soundness 與 completeness 時,是在關心特定屬性 p,我們說一個系統 sound 就是在問「系統能導出的結果是否滿足 p」;我們說一個系統 complete 則是在問「滿足 p 的結果是否皆能從系統導出」。

Theorem A space is Hausdorff iff its diagonal map is closed [math-000B]

The proposition says a space is Hausdorff if and only if its diagonal map Δ:X→X×X\Delta : X \to X \times X to product space is closed, which given an alternative definition. Notice that close means its complement Δ∁\Delta^\complement is open. The definition of Δ\Delta is

Δ={(x,x)∣x∈X} \Delta = \{ (x, x) \mid x \in X \}
Not hard to see below argument also applies to all finite XX-product spaces.

Proof [local-0]

(Hausdorff => diagonal map is closed) Hausdorff means every two different points has a pair of disjoint neighborhoods (U∈Nx,V∈Ny)(U \in \mathcal{N}_x, V \in \mathcal{N}_y), where x∈Ux \in U and y∈Vy \in V. Therefore, every pair (x,y)(x, y) not line on the diagonal has U×VU \times V cover them. The union of all these open sets U×VU \times V covers Δ∁\Delta^\complement, so Δ\Delta the complement of the union is closed.

(diagonal map is closed => Hausdorff) Since

Δ∁={(x,y)∣x,y∈X(x≠y)}\Delta^\complement = \{(x, y) \mid x, y \in X (x \ne y) \}

is open, which implies for all x,yx, y the pair (x,y)∈Δ∁(x, y) \in \Delta^\complement. Since N(x,y)=Nx×Ny\mathcal{N}_{(x, y)} = \mathcal{N}_x \times \mathcal{N}_y, this implies the fact that Δ∁∈Nx×Ny\Delta^\complement \in \mathcal{N}_x \times \mathcal{N}_y.

Also, for all U∈NxU \in \mathcal{N}_x and V∈NyV \in \mathcal{N}_y, it's natural that U×V⊆Δ∁U \times V \subseteq \Delta^\complement, since the open set U×VU \times V at most cover Δ∁\Delta^\complement.

Consider that reversely again, that means for all (x,x)∈Δ(x, x) \in \Delta, pair (x,x)∉U×V(x, x) \notin U \times V (i.e. U×VU \times V will not cover any part of diagonal), that implies U∩V=∅U \cap V = \emptyset as desired.

understanding FF-algebra [math-000E]

To understand FF-algebra, we will need some observations, the first one is we can summarize an algebra with a signature. For example, monoid has a signature:

{1:1→m⋅:m×m→m\begin{cases} 1 : 1 \to m \\ \cdot : m \times m \to m \end{cases}

or we can say ring is:

{0:1→m1:1→m+:m×m→m×:m×m→m−:m→m\begin{cases} 0 : 1 \to m \\ 1 : 1 \to m \\ + : m \times m \to m \\ \times : m \times m \to m \\ - : m \to m \end{cases}

The next observation is we can consider these mm as objects in a proper category CC, at here is a cartesian closed category. For example

  • monoid is a CC-morphism 1+m×m→m1 + m \times m \to m
  • ring is a CC-morphism 1+1+m×m+m×m+m→m1 + 1 + m \times m + m \times m + m \to m

Now, we generalize algebra's definition.

Definition FF-algebra (algebra for an endofunctor) [math-000F]

Consider a proper category CC that can encode the signature of F(−)F(-), and an endofunctor F:C→CF : C \to C, the FF-algebra is a triple:

(F,x,α)(F, x, \alpha)

where α:F  x→x\alpha : F \; x \to x is a CC-morphism.

figure tex154132

With the definition of FF-algebra, FF-algebras of a fixed FF form a category

Definition category of FF-algebras [math-000G]

For a fixed endofunctor FF in a proper category CC (below notations omit fixed FF),

  1. the F-algebras (x,α)(x, \alpha) form the objects of the category,
  2. morphisms are homomorphisms of objects (x,α)→F  m(y,β)(x, \alpha) \xrightarrow{F\;m} (y, \beta), composition is given by functor laws.

A homomorphism is a CC-morphism mm makes below diagram commutes.

figure tex154555

We can extra check identity indeed works.

figure tex154556

There is a theorem about the initial object of the category.

Theorem Lambek's [math-000H]

The evaluator jj of an initial algebra (F,i,j)(F, i, j) is an isomorphism.

Proof [local-0]

Let F  iF\;i be an initial in the FF-algebra category, the following diagram commutes for any CC-object aa

figure tex154979

Now, replace aa with F  iF\;i, we have commute diagram

figure tex154980

Then we consider a trivial commute diagram by duplicating the path F  i→iF\;i \to i

figure tex154981

Combine two diagrams, then we get another commute diagram

figure tex154982

By definition now we know j∘mj \circ m is a homomorphism, and since F  iF\;i is an initial, we must have

F  j∘F  m=1FiF\;j \circ F\;m = 1_{Fi}

Therefore, we also have

j∘m=1ij \circ m = 1_i

so mm is the inverse of jj, proves jj is an isomorphism.

catamorphism [math-000I]

The theorem actually proves the corresponding CC-object ii of initial algebra (F,i,j)(F, i, j) is a fixed point of FF. By reversing jj with its inverse, we get a commute diagram below.

figure tex155405

In Haskell, we are able to define initial algebra

newtype Fix f = Fix (f (Fix f))

unFix :: Fix f -> f (Fix f)
unFix (Fix x) = x

View jj as constructor Fix, j−1j^{-1} as unFix, then we can define m = alg . fmap m . unFix. Since m :: Fix f -> a, we have definition of catamorphism.

cata :: Functor f => (f a -> a) -> Fix f -> a
cata alg = alg . fmap (cata alg) . unFix

An usual example is foldr, a convenient specialization of catamorphism.

anamorphism [math-000J]

As a dual concept, we can draw coalgebra diagram

figure tex155828

and define anamorphism as well.

ana :: Functor f => (a -> f a) -> a -> Fix f
ana coalg = Fix . fmap (ana coalg) . coalg

I mostly learn F-algebras from Category Theory for Programmers, and take a while to express the core idea in details with my voice with a lot practicing. The article answered some questions, but we always with more. What's an algebra of monad? What's an algebra of a programming language (usually has a recursive syntax tree)? Hope you also understand it well through the article and practicing.

Lawvere's fixed point theorem 的陳述與應用 [math-0005]

這篇文章試著介紹 Lawvere's fixed point theorem 的意義與應用

Theorem Lawvere's fixed point [math-0006]

DIAGONAL ARGUMENTS AND CARTESIAN CLOSED CATEGORIES

在一個 cartesian closed category 中,如果 A→ϕBAA \xrightarrow{\phi} B^A 是 point-surjective,則所有 B→fBB \xrightarrow{f} B 都存在不動點 1→sB1 \xrightarrow{s} B(滿足 f∘s=sf \circ s = s)。

Proof [local-0]

首先畫出交換圖

figure tex151599

其中 δ\delta 的定義是 c↦⟨c,c⟩c \mapsto \langle c, c \rangle,所以對 1→pA1 \xrightarrow{p} A 來說 δ∘p=⟨p,p⟩\delta \circ p = \langle p, p \rangle。沿著這個定義,我們知道 (ϕ×1A)∘δ∘p=ϕ∘⟨p,p⟩(\phi \times 1_A) \circ \delta \circ p = \phi\circ \langle p, p\rangle。根據 point-surjective 我們知道 ϕ∘p\phi \circ p 對每個 pp 來說都是唯一確定的。現在把往下方 BB 的 evev 也畫出,即可得出等式:

f∘ev∘ϕ∘⟨p,p⟩=ev∘ϕ∘⟨p,p⟩f \circ ev \circ \phi\circ \langle p, p\rangle = ev \circ \phi\circ \langle p, p\rangle

換句話說 B→fBB \xrightarrow{f} B 的不動點即是

ev∘ϕ∘⟨p,p⟩ev \circ \phi\circ \langle p, p\rangle

Example 應用到 lambda calculus [math-0007]

編碼的方式是找一個只有兩個物件 1,T1, T 的 category MM,讓 lambda calculus 的 terms 是 MM-morphisms,這些技巧在 Categorical semantic 領域內相當常見。

到這裡必須檢查 lambda calculus 確實存在 point-surjective,歡迎自行嘗試。

按此編碼後可以得到 ev∘ϕ∘⟨p,p⟩→encodeϕ(p)(p)ev \circ \phi\circ \langle p, p\rangle \xrightarrow{encode} \phi(p)(p)。套到等式上

f∘ev∘ϕ∘⟨p,p⟩=ev∘ϕ∘⟨p,p⟩f \circ ev \circ \phi\circ \langle p, p\rangle = ev \circ \phi\circ \langle p, p\rangle

可以得到以下編碼

f(ϕ(p)(p))=ϕ(p)(p)f(\phi(p)(p)) = \phi(p)(p)

把 ϕ(p)\phi(p) 改寫成 qq,於是等式可以寫成 q(p)=f(ϕ(p)(p))q(p) = f(\phi(p)(p)),而函數套用語法可以用 λ\lambda 退化,於是得到

q=λx.f(ϕ(x)(x))q = \lambda x. f(\phi(x)(x))
注意到 qq 的定義是非遞迴的,因此一定是可定義的。

計算即可發現確實 q(p)=ϕ(p)(p)=f(ϕ(p)(p))q(p) = \phi(p)(p) = f(\phi(p)(p))。因為所有的 λ\lambda-term 都屬於 TT,所以現在我們知道任何 lambda calculus 的函數都有不動點。

Example 應用到 Cantor's theorem [math-0008]

取 category of sets Sets\bold{Sets} 並令 B=2B = 2 就可以證明 Cantor's theorem。我們需要引理

Lemma no point-surjective morphism [math-0009]

If there exists B→fBB \xrightarrow{f} B such that f∘b≠bf \circ b \ne b for all 1→bB1 \xrightarrow{b} B, then there has no point-surjective morphism can exist for A→gBAA \xrightarrow{g} B^A.

This is a reversed version of Lawvere's fixed point, no need an extra proof.

Proof [local-0]

存在 2→not22 \xrightarrow{\text{not}} 2 令所有 1→b21 \xrightarrow{b} 2 都滿足 not∘b≠b\text{not} \circ b \ne b,所以對任何集合 AA 都不存在 A→2AA \to 2^A 的 point-surjective 函數,因此也不存在 A≅2AA \cong 2^A。

這個定理也可以用到 Gödel incompleteness、Tarski definability 等等問題上。論文 Substructural fixed-point theorems and the diagonal argument: theme and variations 更進一步的簡化了前置條件。

子類型與多型(泛型) [tt-000L]

這個文章源自下面的問題

不過想問問專業的⋯⋯Golang 都有 interface 這種東西,提供泛型的必要性是什麼啊?


這是個常見的誤解,因為大部分語言不會多認真跟你講清楚到底啥是啥,看來好像都能接受多種不同的類型輸入不是嗎?所以這裡我們就來看各種機制的定義跟差別。

Go 的 interface [tt-000M]

Go 語言的 interface,範例如下

type Abser interface {
        Abs() float64
}

其中的 Abs() float64 可以被改寫成抽象語法 Abs : () -> float64。因此一個 interface AA 有一組與其關聯的一系列簽名 P^\hat P。並且,對 Go 而言下列的兩個 interface 並沒有差異

type A interface {
        F(C) D
}
type B interface {
        F(C) D
}

因此我們可以更進一步認為 Go 的一個 interface AA 可以完全由其簽名 P^\hat P 替換。

Java 的 interface 與 bounded polymorphism (bounded quantification) [tt-000N]

在 Java 之中可以定義 interface,範例如下

interface Comparable {
  int compareTo(T other);
}

相較於 Go 中不用明確宣告,在 Java 中實現 interface I 需要寫成 class T extends I,因此雖然 Java 的 interface AA 也有其相關的一組簽名 P^\hat P,但兩個有同樣簽名 P^\hat P 的 interface 不是同一個型別。

而在 Java 中 bounded polymorphism 指的是以下的 interface 使用方法:

<S extends Comparable> S min(S a, S b)

這會使得沒有明確宣告實現 Comparable 的型別無法被傳為 min 的引數。

問題所在 [tt-000O]

現在考慮一個簽名

f:(S≤A)⇒S×S→Sf : (S \le A) \Rightarrow S \times S \to S

在 Java 裡面可以確實保證兩個參數跟回傳的型別都是同一個。在 Go 裡面這個簽名就只能寫成

f:P^×P^→P^f : \hat P \times \hat P \to \hat P

而兩個參數跟回傳的型別不一定相同。現在我們就會發現乍看之下 interface 對類型的約束跟多型一樣可以被多個不同的型別滿足,但事實上根本就是不同的東西。

interface 是一種子類型關係,換句話說當 TT 實現 AA 我們就說 T≤AT \le A,在每個要求 AA 的位置都可以用 TT 取代。

多型的本體是一個型別等級的參數!在語意學上,上面的簽名可以改寫成 f:ΛS.S×S→Sf : \Lambda S. S \times S \to S,當你寫下

f  kf \; k

的時候,乍看之下是一個完整的呼叫。實際上編譯器會先推導 kk 的類型,記作 ↑k\uparrow k,譬如假設 k:Ck : C 則 ↑k=C\uparrow k = C,這時候就可以做型別替換 (ΛS.S×S→S)C(\Lambda S. S \times S \to S) C 得出 C×C→CC \times C \to C。因此真正的呼叫程式碼是

f  [C]  kf \; [C] \; k

子類型規則並不能取代替換這個語意。在 Java 的 f:(S≤A)⇒S×S→Sf : (S \le A) \Rightarrow S \times S \to S 規則下,僅是多檢查了 C≤AC \le A 是否為真,子類型與多型仍然是不同的規則。

Gödel incompleteness theorem 到底證明了什麼? [math-0002]

哥德爾不完備定理是一個相當有趣的邏輯學發現,要了解他的意涵,要先考慮哥德爾的編碼

Definition Gödel encoding [math-030A]

哥德爾數(Gödel number)是一種編碼方式,在一個算術系統裡面,當我們把每個字符都排成一個有限的列,我們可以幫每個字符都指定一個自然數。具體來說,我們可能會寫下

  1. ¬\neg 是 11,其後設意義為「否定」
  2. ∨\lor 是 22,其後設意義為「或」
  3. ∧\land 是 33,其後設意義為「且」

當有符號 si(i∈N)s_i (i \in \N),其對應的哥德爾數為 nin_i。我們並不是真的關心具體的每個符號,我們做這件事的理由是為了表示哥德爾編碼。所以這裡要開始定義哥德爾編碼,首先先看一個簡單的例子:當有系統的公式符號排列 s1s10s4s_1 s_{10} s_4,我們說公式的哥德爾編碼為 2n1⋅3n10⋅5n42^{n_1}\cdot 3^{n_{10}}\cdot 5^{n_4}。

因此用函數 α(i)\alpha(i) 指示一個公式的每個位置 ii 符號的 Gödel number,則哥德爾編碼確切的定義是

∏i∈Ipinα(i)\prod_{i \in I}p_i^{n_{\alpha(i)}}

,也就是將質數以每個符號對應的哥德爾數為指數,將它們全部乘起來。這個辦法只是為了迫使每個公式有對應的編碼存在。我們用 G‾\overline{G} 記號表示公式 GG 的哥德爾數。

α\alpha 函數給出第 ii 個出現符號的在表中的位置。

在承認前述的系統 PP 下,可以開始描述。

Definition substitution [math-0004]

x[m/n]x[m/n] 表示將公式 xx 中哥德爾數為 nn 之符號換成 mm 的哥德爾數再求此替換後公式的哥德爾編碼。

例如 ¬y[n/y‾]=¬n‾\neg y[n/\overline{y}] = \overline{\neg n}

Theorem Gödel incompleteness [math-0003]

  1. 存在形式上不能證明卻是真確、可寫下的公式
  2. 用系統證明自身的一致性會導致不可決定的公式也被證明

Proof [local-0]

首先,用 ∃x.x⊢k\exists x. x \vdash k 表示有哥德爾數為 kk 的公式在系統 PP 中得到證明,而 xx 是其演示

哥德爾有一段很長的論證(證明這是原始遞歸函數),來說明這是可以被 PP 表示的,這裡跳過了這部分論證。且這也是為什麼要求系統 PP 是具備算術能的,因為表示編碼需要自然數算術。

定義 n=¬∃x.x⊢y[y/y]n = \lnot\exists x. x \vdash y[y/y],但 yy 是未知量,隨意給一個還沒被使用來編碼的數字即可,比如 y‾\overline{y}。於是我們重寫成 n=¬∃x.x⊢y[y/y‾]n =\lnot\exists x. x \vdash y[y/\overline{y}]。

現在構造 G=¬∃x.x⊢n[n/y‾]G = \lnot\exists x. x \vdash n[n/\overline{y}],求其哥德爾編碼 G‾\overline{G} 可以發現正是 n[n/y‾]n[n/\overline{y}] 所表示的編碼,因為

n[n/y‾]=¬∃x.x⊢n[n/y‾]‾=G‾n[n/\overline{y}] = \overline{\lnot\exists x. x \vdash n[n/\overline{y}]} = \overline{G}

因此用 G‾\overline{G} 替換 GG 中的 n[n/y‾]n[n/\overline{y}],會得到

¬∃x.x⊢G‾\lnot\exists x. x \vdash \overline{G}

可以發現 GG 的後設含義正是「有哥德爾編碼為 G‾\overline{G} 的公式沒有系統 PP 中的證明」,然而

  1. 假設 GG 可以在 PP 中證明為真,就告訴我們 G‾\overline{G} 對應的公式不存在任何演示可以於 PP 中證明,但那就是我們剛證明的公式 GG。
  2. 假設 GG 可以在 PP 中證明為否,就導出 G‾\overline{G} 對應的公式在 PP 中有演示,但那就是我們剛證否的公式 GG。

綜合可知這意思就是 GG 可證,若且唯若 GG 可證否,是故承認 GG 就是承認 PP 不一致;反之,若 PP 一致則 GG 形式上就不可決定。

巧妙的是,並不難看出 GG 的 meta 陳述是真確的,因為確實不存在任何整數作為編碼滿足這個性質。換句話說,存在形式上不能證明卻是真確、可寫下的公式,這就是第一不完備定理。接著根據第一不完備定理知道了 GG 的真確性,卻在 PP 中形式上不可決定,所以 PP 不能證明一個真確的算術公式,因此 PP 必定是不完備的,這就是第二不完備定理。

因此在 The Blind Spot: Lectures on Logic 中可以看到其陳述更加關注核心:對一個足夠編碼自己又一致的系統

  • 存在一個真確的公式 GG 不可決定
  • 系統證明自己具有一致性會連帶證明這個不可決定的公式,所以系統無法證明自己的一致性

換句話說,要關心的並不是具體如何構造上面陳述的編碼,而是這類系統編碼系統自身的普遍性問題。

型別有多大? [tt-000E]

不久之前看到fizzyelt 的文章,所以我前幾日開始研究自己以前寫的 strictly positive 筆記,因此找到了 Andrej Bauer 在 stackexchange 的回答,整理後寫了這篇文章解釋何謂型別的大小

型別的大小 [tt-000F]

要了解型別的大小的意義,可以先考慮一個 small cartesian closed category CC,令型別是範疇 CC 的物件(視為只有一個型別的 context)。

應用 slice category [tt-000H]

在這個情況下,對任意型別 tt,slice category C/tC/t 表示「到達 tt 的 morphism」所構成的範疇,也就是 C/tC/t 的 objects 與 triangle morphisms。

Example 從 11 出發 [tt-000I]

接著,我們要關心其中沒定義的 morphism,我們將基於此集合討論型別 TT 有多大。技術上來說,是指從 terminal object 11 出發的所有 morphisms(記為 HomC(1,T)\text{Hom}_{C}(1, T)),這些 morphisms 對應到內建的形式;以邏輯來說是指公理,而在函數式語言裡面,我們最常看到的定義公理的方式就是 data type!換句話說,型別的大小由建構子決定:

  • 00 是 False / Empty type,沒有任何建構子
  • 11 是 Unit type,只有一個建構子
  • 22 是 Bool type,只有兩個建構子

依此類推可以知道有 kk 個單元建構子(即建構子 c 滿足 c : K )的型別 K 可以解釋為 kk-type,接著 2→T2 \to T 的元素應該有幾個呢?答案是 T×T=T2T \times T = T^2,同理我們可以建立 k→T=T×...×T=Tkk \to T = T \times ... \times T = T^k 的關係式。

依此可以建立一個簡單的直覺是 c : T 表示 +1+1,而更加複雜的型別如 c : (2 -> T) -> T 就表示 +T2+T^2,建構子的參數表現了型別增長的速度,決定了型別的大小。

一個常見的案例是自然數,我們通常定義 z:Nz : \N 和 s:N→Ns : \N \to \N 兩個建構子來表示自然數 N\N。用 F-algebra 來表示,可以得到 (1+N)→N(1 + \mathbb{N}) \to \mathbb{N},從沒有循環參考的角度來看,當前 N\N 的大小即取決於上一個 N\N 的大小。畫出交換圖就可以了解到從基本點 zz 出發,只有 ss 一個方向可以前進,恰恰就是可數無限大的定義,是以看到這與歸納法的對應時,應當不會太過驚訝。

inductive data type [tt-000J]

在 dependent type 語言裡面,data type 會變成 inductive data type,引入了更多的限制,而其中跟大小有關的限制就是這裡想要討論的。

Question 在定義建構子時 c:(T→T)→Tc : (T \to T) \to T 有參數 TTT^T ,試問 TTT^T 有多大? [local-0]

答案是不知道,因為我們根本還不知道 TT 到底是什麼。

當 TT 定義完畢之後,這個簽名用在函數上倒是沒有問題,因為其大小已經確定,不會遇到自我指涉引發的麻煩。

雖然可以用公理強迫 TTT^T 有 size,但這樣做一來會破壞計算性;二來會顯著的複雜化 termination check。舉例來說,要是容許定義 c,就可以寫出

def foo (t : T) : T :=
  match t with
  | c f => f t

接著 foo (c foo) 就會陷入無限迴圈,為了要有強正規化,我們希望排除所有這類型的程式,比起一個一個去比對,直接禁止定義 c 這類的建構子可以更簡單的達成目的。Haskell/OCaml 的 data type 則不做此限制,因為它們不需要強正規化,這也是為什麼 dependent type 語言常説其 data type 是 inductive data type,並不輕易省略 inductive 這個詞,因為這樣定義的類型滿足 induction principle。

另外一提,Bauer 談到所有建構子都是為了符合 c:Πx:B(A  x→T)→Tc : \Pi_{x : B} (A \; x \to T) \to T 的形式,有興趣的讀者可以著手證明。此外實現檢查機制時,strictly positive 的遞迴定義可能更適合作為參考。

遞迴 [tt-000G]

把程式作為 object,而 continuation 作為 morphism,就可以發現尾遞迴完全對應到迴圈這個抽象,兩者都有

  • b:c→kb : c \to k base case 與 break
  • n:c→cn : c \to c induction case 與 next step

這兩個 morphism。從構造的角度來看,還可以說他們跟自然數有對應,上面的簽名恰恰正是 initial 跟 step。

分形遞迴(Tree Recursion) [tt-000K]

典型的情況是 fibonacci 數列,展示了前面的型別大小如何與程式相關,常見的程式定義如下

def fib : Nat → Nat
  | 0   => 1
  | 1   => 1
  | n+2 => fib n + fib (n+1)

為了抽出計算部分的代數,我們用 inductive type 定義其計算

inductive Fib (a : Type) : Nat → Type where
  | z : a → Fib a 0
  | o : a → Fib a 1
  | s : Fib a n → Fib a (n+1) → Fib a (n + 2)

可以看到 induction case 就是 (2→Fa)→Fa=Fa2→Fa=Fa×Fa→Fa(2 \to F a) \to F a = {F a}^2 \to F a = F a \times F a \to F a,所以對應的程式就是兩次呼叫 fib n + fib (n+1) 。當然,因為 fibonacci 還有一個利用矩陣計算的編碼

[Fk+2Fk+1]=[1110][Fk+1Fk]\left[ {\begin{array}{cc} F_{k+2} \\ F_{k+1} \\ \end{array} } \right] = \left[ {\begin{array}{cc} 1 & 1 \\ 1 & 0 \\ \end{array} } \right] \left[ {\begin{array}{cc} F_{k+1} \\ F_k \\ \end{array} } \right]

這個方法對應到 O(n)O(n) 的演算法,因此 fibonacci 不一定要用分形遞迴計算。這件事也告訴了我們代數簽名有時候比我們具體的計算更寬鬆,上面的 inductive type 並未限制如何使用 s,也因此可以摺疊時可能存在更快的計算方式。這也說明了當遇到 c : (T -> T) -> T 一類的建構子時,其對應的計算難以寫出的問題,也就是前面沒辦法定義 TTT^T 的大小的情形。

Parser combinator:文法規則就是組合子的運用 [cs-0009]

Definition 左遞迴 [cs-000A]

左遞迴是我們定義文法的時候會出現的一種形式,比如

Expr→Expr+ExprExpr \to Expr + Expr

翻譯成遞歸下降解析器時會有像下面這樣的程式

def expr : Parsec E :=
  let l <- expr
  match '+'
  let r <- expr
  return E.add l r

這時候 expr 顯然是一個非終止的發散函數,而因為遞迴的原因是運算式的左邊,所以被稱為左遞迴。這樣的 infix 運算形式在文法中很常見,所以我們需要方法來消除文法中的左遞迴。

Definition Parser combinator [cs-000B]

解析器組合子(parser combinator)最早來自論文 A new top-down parsing algorithm to accommodate ambiguity and left recursion in polynomial time, 在現在的使用中因為不同語言特性跟 parser combinator 指涉的比較像是一種程式方法的情況下,本文中的 parser combinator 是單純指把 parser 跟 higher order function 結合的一種程式方法。這個方案最大的好處就是遞歸下降解析跟程式寫法一一對應,因此相當直覺。

消除左遞迴的方法 [cs-000C]

現在你知道為什麼我們需要了解遞歸下降解析中消除左遞迴的方式,以下面文法來說

Expr→Expr+Expr∣Int∣ExprExpr \to Expr + Expr \\ \mid Int \\ \mid Expr

我們可能會將定義改成

Expr→Term+TermTerm→Int∣ExprExpr \to Term + Term \\ Term \to Int \mid Expr

這個解法在對應的 parser combinator 中看起來像是

mutual
  def term := parse_int <|> parens expr
  def expr : Parsec E :=
    let l <- term
    match '+'
    let r <- term
    return E.add l r
end

擴展到 operator 優先序問題 [cs-000D]

上面的方法解決了左遞迴問題,但是要是有多個不同優先級的運算形式就會讓文法快速膨脹

Expr→Term+TermTerm→Factor∗FactorFactor→Int∣ExprExpr \to Term + Term \\ Term \to Factor * Factor \\ Factor \to Int \mid Expr

這樣單調的修改在文法裡面確實很無聊,不過只要仔細觀察對應的 combinator,就會它們的型別發現這告訴了我們要怎麼利用 higher order function 來解決重複的問題!對 infix 運算形式(左結合)來說,通用的程式是

def infixL (op : Parsec (α → α → α)) (tm : Parsec α) : Parsec α := do
  let l ← tm
  match ← tryP op with
  | .some f => return f l (← tm)
  | .none => return l

這個 combinator 先解析左邊的 lower tm ,接著要是能夠解析 op 就回傳 op combinator 中存放的函數,否則就回傳左半邊的 tm 結果。使用上就像這樣

mutual
  def factor : Parsec Expr

  def expr : Parsec Expr :=
    factor
    |> infixL (parseString "*" *> return Expr.mul)
    |> infixL (parseString "+" *> return Expr.add)
end
  1. 其中 parseString 是個假設會進行 whitespace 過濾並 match 目標字串的 combinator
  2. Expr.mul 跟 Expr.add 的型別被假設為 Expr → Expr → Expr

這裡真正發生的就是使用組合子編碼了文法的層級,其中每個規則都被記錄成匿名函數,我們無需再另外命名。

處理優先度相同的運算子 [cs-000E]

因為有些運算子優先級相同,所以我們還需要再修改上面的函數來正確的支援這個概念;而且上面的程式為了避免複雜化,只解析一次 operator 加上 tm 就退出了,會導致實際上 1 + 2 * 3 * 4 這種算式的 * 4 部分沒有被解析。要解決這個問題,我們需要貪婪解析右半邊 op tm 的部分。綜上所述我們得到的程式碼是

def infixL (opList : List (Parsec (α → α → α))) (tm : Parsec α)
  : Parsec α := do
  let l ← tm
  let rs ← many do
    for findOp in opList do
      match ← tryP findOp with
      | .some f => return (f, ← tm)
      | .none => continue
    fail "cannot match any operator"
  return rs.foldl (fun lhs (bin, rhs) => (bin lhs rhs)) l

首先我們有一串 operator 而非一個,其次在右邊能被解析成 op tm 時都進行解析,最後用 foldl 把結果轉換成壓縮後的 α

我希望這次有成功解釋怎麼從規則對應到 parser combinator,所以相似的規則可以抽出相似的 higher order function 來泛化,讓繁瑣的規則命名變成簡單的函數組合。這個技巧當然不會只有在這裡被使用,只要遇到相似形的文法都可以進行類似的抽換,相似的型別簽名可以導出通用化的關鍵。

Appendix 其他 expression combinator [cs-000F]

在這裏提到的所有程式都實作在我幫 lean 寫的一個解析器擴充程式庫 https://git.sr.ht/~dannypsnl/parsec-extra 中

右結合的 infix

partial def infixR (opList : List (Parsec (α → α → α))) (tm : Parsec α)
  : Parsec α := go #[]
  where
  go (ls : Array (α × (α → α → α))) : Parsec α := do
    let lhs ← tm
    for findOp in opList do
      match ← tryP findOp with
      | .some f => return ← go (ls.push (lhs, f))
      | .none => continue
    let rhs := lhs
    return ls.foldr (fun (lhs, bin) rhs => bin lhs rhs) rhs

prefix op tm

def «prefix» (opList : List $ Parsec (α → α)) (tm : Parsec α)
  : Parsec α := do
  let mut op := .none
  for findOp in opList do
    op ← tryP findOp
    if op.isSome then break
  match op with
  | .none => tm
  | .some f => return f (← tm)

postfix op tm

def «postfix» (opList : List $ Parsec (α → α)) (tm : Parsec α)
  : Parsec α := do
  let e ← tm
  let mut op := .none
  for findOp in opList do
    op ← tryP findOp
    if op.isSome then break
  match op with
  | .none => return e
  | .some f => return f e

什麼是啟動編譯器? (bootstrap compiler) [cs-0001]

當一個語言逐漸成熟,開發者常常會有一個想法就是用自己開發的語言寫這個語言的編譯器,但是這個決策會導致程式庫中有兩套目標一樣但是實現語言不同的編譯器程式。其一是本來的實現,另一個是用語言自身新寫的。一來這樣開發任何新功能的時候都需要寫兩次;二來這會減少可能的參與者人數,因為這下他得要熟悉兩個語言才行!解決這個問題的辦法就是啟動編譯器,通常是一個在多平台上可以直接執行的檔案。

運作原理 [cs-0002]

約略是下面描述的樣子:

  1. 語言本身後端的目標語言可以生成這個啟動編譯器,讓我們先用可執行檔簡單理解它,後面再闡述為什麼用普通的可執行檔可能不是一個好主意
  2. 每次編譯器用到新的特性的時候,都需要更新啟動編譯器檔案並提交進程式庫
  3. 有了上述準備,就可以在第一次編譯編譯器自身的時候,用啟動編譯器編譯出最佳化過的編譯器執行檔

現在有了最佳化過的編譯器執行檔,就可以正常的使用語言自己對語言自己進行開發了。

現在可以來想一下為什麼是這樣的流程,首先一個程式語言除非被大量作業系統支援(比如 C、Python),否則系統上是不會有這個程式語言的編譯器的!這樣問題就來了:假設有人試圖參與這個編譯器專案,用的平台剛好跟既有開發者都不一樣,請問要由誰提供已經用語言自身編寫的編譯器的編譯器呢?想當然除了一些喪心病狂的作法,是不可能完成這個任務的!所以一個無論如何都已經可以執行的編譯器,哪怕效能比較差,也具有存在的價值。

而語言自身的編譯器用到新功能的時候,也一定要更新啟動編譯器,否則下一輪的新參與者就沒辦法正確的進行初始編譯。

用普通的可執行檔可能不是一個好主意 [cs-0003]

前面提過說啟動編譯器通常是一個在多平台上可以直接執行的檔案,也提過哪怕效能不佳也可以,這兩個理由使得特定平台的可執行檔不是一個好的選擇。 當然,我們也可以選擇維護每個平台的啟動編譯器,chez scheme 就維護了一堆根據不同平台套用組合的二進位檔案做這件事。

回到正題上,啟動編譯器通常不會選擇這麼複雜的方式,而是會生成可以在多平台上執行的檔案。舉例來說,生成 C 或是 Python 這種已經被許多系統廣泛支援的語言,最近 zig 就是使用 wasm 達成這個目標。但這個選擇也不能太過隨便,比如要是語言自己的 type checking 任務很長,生成 Python 就會得到一個大家都不想等它跑完就不玩了的啟動編譯器。而限制太多的語言像是 haskell 或是 rust 可能也不是優秀的選擇,因為你的語言模型很可能跟這些目標語言非常不相配,進而讓編譯過程非常的痛苦。

有了這些概念,我想你已經有能力為自己的語言寫出啟動編譯器了!那麼剩下的篇幅我就可以來寫點 murmur,我覺得這個概念可以進一步推升到選擇一個夠容易編譯出來,也夠容易寫出通用最佳化編譯器的語言來達成。我個人是很看好 wasm,因為它確實容易當目標、最佳化,只可惜現在還有不少重要的功能都沒有定案。llvm 的 bytecode 確實也是一個方案,寫一個把 llvm bytecode 編譯成 executable 的 python 檔案並不困難,但 llvm 的連結性自己就是一個天殺的大麻煩所以這個方案我很少看到有人使用。Java bytecode 則是讓那些本來就預計只在 JRE 生態系內部的語言從一開始就沒有這個問題,微軟的 CLR 也是類似的狀況,缺陷是最後語言受制於 runtime,這個問題會有多大條取決於語言的目的是什麼。

好像是今年最後一天了,祝你不是在今天看到這篇文章的!

polymorphism 的執行期設計 [cs-0000]

在傳統的 ML 或是 Haskell 裡面,有一個型別表現的特別醜陋,就是 data type 表示的 disjoint。 這是因為無論如何,disjoint 就是會需要執行期時有要處理哪個分支的資訊。 因此語言中都設計有 constructor 這麼一個特殊的存在,其中每個 case 都有對應的執行期標籤。 舉例來說,

type bool = false | true

這裡 false 可能就是 0 而 true 是 1。

type nat = z | s of nat

這裡 z 可能就是 0 而 s 是 1 並且接有一個 tuple 放 nat 。

要是這種 disjoint 語義想要完美對應 category theory 的 coproduct,那就需要引入真正的 union type。 要注意到這跟 C 語言的 union 略有不同,C 語言事實上仰賴了開發者完全知道自己在做什麼作為前提, 要求開發者對 union 正確的操作。要是要求 C 語言一樣要能處理不同型別的輸入的話,一樣要放置一個標籤欄位才能解決。 union type 在型別上是很優美,沒有 多餘 的標籤,在 typed/racket 裡面可以記成 (U Integer Boolean String) 之類的。但這個方案遠非毫無缺點,之所以可以這麼做,事實上是因為 racket(以及早期的諸多 functional languages)採用了 universal object 編碼。

Universal object 就是指對每一個執行期的物件,都把型別囊括在內。 這樣一來當然就不需要另外給標籤,因為每個物件本來就都有標籤了。 可想而知,這個方法缺點就是針對需要緊密排列的運用場景效果不佳。 所以 racket 要是需要操作 vector float 這種資料,再怎麼編譯都還是會慢 C 語言一線, 主因就是沒有用最緊密的方式排列資料(多了標籤),相較於 C 沒有利用到當代硬體特性。 但反過來說就沒有 ad-hoc 的編譯難題,因為本來就是需要 runtime 解決的問題就推到 runtime 解決。

那麼可想而知,後來為了適當壓縮物件,ML 跟 Haskell 都走上混合的解決方案。 針對小物件(通常是指小於等於該平台的一個 word 的資料型別)就會盡可能不要包起來,反之就採用 universal object, 在實務上表現也確實不錯。

利用參考等價性質的技巧 [racket-0000]

所謂的使用參考等價性,是指在某些語言裡面,兩個物件是否相同是通過參考而非內容決定的。 舉例來說, racket 裡寫下一個空結構,然後生成一些實例

(struct id ())

(define a (id))
(define b (id))
(define c a)

我們可以用一些 eq? 運算來檢查

> (eq? a b)
#f
> (eq? a c)
#t
> (eq? c a)
#t
> (eq? b c)
#f

這樣就可以生成一連串在運行中的程式內唯一的資源標示。

但要小心你用的語言的特性,比如要是我把上面的 (struct id ()) 變成 (struct id () #:transparent) 並改用 equal? 運算, racket 就會改用內容而非參考去判定相等。

應用場景 [local-0]

那麼這種技巧可以用在哪些地方呢?首先要注意到這絕對不可以用在序列化上!每次程式重新啟動,同一個宣告都會被重新分配到不同的參考,所以這不能超出程式運行的範疇。 所以合適的用法應該是在程式終止之後就不會再仰賴這個資源唯一性的。

  1. Racket 自己 Sets of Scopes:在 Matthew Flatt 的 Let's Build a Hygienic Macro Expander 裡, scope 就是一個空結構。
  2. 型別檢查裡面的自由變數也可以用這個方式生成(這樣我們就不用維護一個 free variables counter)。
  3. Threading ID: 共時結構要是需要唯一標示的時候,這個技巧就很方便,因為沒有正確運作的記憶體配置器會把物件分配到同一個地方。

這樣的小技巧非常方便,當然也需要注意其限制以及程式語言的實際運作方式。 在使用前最好真的去讀你的語言對參考的處理方式,確保這些程式仰賴的環境假設正確無誤,才不會出現出乎意料的問題。

Leibniz product rule [math-7EIK]

考慮 F(x)=f(x)g(x)F(x) = f(x)g(x),求F(x)′F(x)'? 直覺上會因為 f(x)+g(x)f(x)+g(x) 的微分是 f(x)′+g(x)′f(x)'+g(x)',而認為應該定義成 f(x)′g(x)′f(x)'g(x)'。那我們馬上實驗看看?令 f(x)=xf(x) = x 且 g(x)=xg(x) = x,得

(x⋅x)′=x′⋅x′=1⋅1=1(x\cdot x)' = x' \cdot x' = 1 \cdot 1 = 1

這顯然是荒謬的結論,所以這應該是錯的。不如回歸本來的定義?

F(x)′=lim⁡t→0F(x+t)−F(x)tF(x)' = \lim_{t \to 0} \frac{F(x+t) - F(x)}{t}

因為我們已經知道 F(x)=f(x)g(x)F(x)=f(x)g(x),於是

lim⁡t→0F(x+t)−F(x)t=lim⁡t→0f(x+t)g(x+t)−f(x)g(x)t\lim_{t \to 0} \frac{F(x+t) - F(x)}{t} = \lim_{t \to 0} \frac{ f(x+t)g(x+t) - f(x)g(x) }{t}

接下來需要一點符號操作,我們讓分子的部分加上 f(x)g(x+t)f(x)g(x+t) 再減掉 f(x)g(x+t)f(x)g(x+t),結果還是同一個運算式。再用一些重排組合運算式的方式化簡lim⁡t→0g(x+t)=g(x)\lim_{t \to 0}g(x+t) = g(x) follows from the fact that differentiable functions are continuous.。

lim⁡t→0f(x+t)g(x+t)−f(x)g(x)t=lim⁡t→0f(x)g(x+t)−f(x)g(x+t)+f(x+t)g(x+t)−f(x)g(x)t=lim⁡t→0(f(x)g(x+t)−f(x)g(x))+(f(x+t)g(x+t)−f(x)g(x+t))t=lim⁡t→0f(x)[g(x+t)−g(x)]+[f(x+t)−f(x)]g(x+t)t=lim⁡t→0f(x)[g(x+t)−g(x)]t+lim⁡t→0[f(x+t)−f(x)]g(x+t)t=f(x)⋅lim⁡t→0[g(x+t)−g(x)]t+lim⁡t→0[f(x+t)−f(x)]g(x+t)t=f(x)⋅lim⁡t→0[g(x+t)−g(x)]t+lim⁡t→0[f(x+t)−f(x)]t⋅lim⁡t→0g(x+t)=f(x)g(x)′+f(x)′g(x)\begin{aligned} &\lim_{t \to 0} \frac{ f(x+t)g(x+t) - f(x)g(x) }{t} \\=& \lim_{t \to 0} \frac{ f(x)g(x+t) - f(x)g(x+t) + f(x+t)g(x+t) - f(x)g(x) }{t} \\=& \lim_{t \to 0} \frac{ (f(x)g(x+t) - f(x)g(x)) + (f(x+t)g(x+t) - f(x)g(x+t)) }{t} \\=& \lim_{t \to 0} \frac{ f(x)[g(x+t) - g(x)] + [f(x+t) - f(x)]g(x+t) }{t} \\=& \lim_{t \to 0} \frac{f(x)[g(x+t) - g(x)]}{t} + \lim_{t \to 0} \frac{[f(x+t) - f(x)]g(x+t)}{t} \\=& f(x) \cdot \lim_{t \to 0} \frac{[g(x+t) - g(x)]}{t} + \lim_{t \to 0} \frac{[f(x+t) - f(x)]g(x+t)}{t} \\=& f(x) \cdot \lim_{t \to 0} \frac{[g(x+t) - g(x)]}{t} + \lim_{t \to 0} \frac{[f(x+t) - f(x)]}{t} \cdot \lim_{t \to 0}g(x+t) \\=& f(x)g(x)' + f(x)'g(x) \end{aligned}

Generalizations [local-0]

現在想想要是 F(x)=f(x)g(x)h(x)F(x) = f(x)g(x)h(x)?為了方便,接下來改記錄成 F=fghF=fgh。首先,其實很容易想到 F′=fg⋅h′+(fg)′⋅hF'=fg\cdot h' + (fg)'\cdot h,於是

fgh′+(fg)′h=fgh′+(fg′+f′g)h=fgh′+fg′h+f′ghfgh' + (fg)'h \\= fgh' + (fg' + f'g)h \\= fgh' + fg'h + f'gh

如果你也操作了一次四個函數的積的結果,你應該已經猜到我們觀察到的模式就是:每個子函數分別微分一次。顯然我們會想要證明這個結果,所以我們假設有函數 g=f1f2⋯fkg=f_1f_2 \cdots f_k。要論證的是隨便加個函數當 fk+1f_{k+1} 會怎樣(再次運用已經證明的兩項次事實):

(f1f2⋯fkfk+1)′=(gfk+1)′=g′fk+1+gfk+1′(f_1f_2 \cdots f_k f_{k+1})' \\= (g f_{k+1})' \\= g' f_{k+1} + g f_{k+1}'

這表示我們的猜測無誤,每次確實都會發生新加入的函數被微分一次的事實,於是我們可以總結出模式:

∏i=1kfi=f1f2⋯fk=(f1′f2⋯fk)      第1項+(f1f2′⋯fk)      第2項+⋯                                第i項+(f1f2⋯fk′)      第k項\begin{aligned} \prod_{i=1}^{k} f_i =& f_1f_2 \cdots f_k \\=& (f_1'f_2 \cdots f_k) \;\;\; 第 1 項\\ +& (f_1f_2' \cdots f_k) \;\;\; 第 2 項\\ +& \cdots \;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\; 第 i 項\\ +& (f_1f_2 \cdots f_k') \;\;\; 第 k 項\\ \end{aligned}

運用Generalizations [local-1]

考慮到常見的函數:f(x)=x1nf(x) = x^\frac{1}{n},求 f(x)′f(x)'?因為剛看完Leibniz product rule,我們可以想到函數 Id(x)=x=f(x)n=(x1n)nId(x)=x=f(x)^n=(x^\frac{1}{n})^n。運用剛剛證明的多項次結果,可以得到

Id(x)′=1=n⋅fn−1⋅f′Id(x)'=1 \\= n \cdot f^{n-1} \cdot f'

於是可以由 1=n⋅fn−1⋅f′1 = n \cdot f^{n-1} \cdot f' 導出

f′=1n⋅fn−1=1n⋅f1−n=1n⋅(x1n)1−n=1n⋅x1n−1f' = \frac{1}{n \cdot f^{n-1}} \\= \frac{1}{n} \cdot f^{1-n} \\= \frac{1}{n} \cdot (x^\frac{1}{n})^{1-n} \\= \frac{1}{n} \cdot x^{\frac{1}{n}-1}

這是個有趣的結論。

closure conversion [cs-N1SG]

Closure conversion is an important transformation in compilers, especially for functional programming languages. To know why this is important, you should know what closure is. To understand closure, lambda calculus is a prerequisite, and I assume you are already familiar with it.

Closure [local-0]

The closure is a lambda that has captured external variables, to move forward, I must introduce the definition

  1. external variables is the variable not defined in the lambda body, also not a parameter
  2. captured a variable is having expression which is using it

An example always helps us learn a concept better, in the following lambda, z is captured, but x is not captured.

(let ([z 123])
(lambda (x)
  (+ z x)))

Now, you should know what's a closure roughly, but why do we need to convert it?

Assembly Model [local-1]

That's because, in assembly or similar models(for example, C programming language), one cannot keep an external variable, at least not generally. For example, you can refer to a global variable(an external of course) in C.

int c = 1;
void foo() {
  c = 2;
}

how about a variable in the stack?

int bar() {
  // what to do here?
  return c + 1;
}
typedef int (*bar_t)();

bar_t foo(int n) {
  int c = n;
  return &bar;
}

In this case, when the caller uses this result, c is already gone! This shows you need to save it into the heap, and this is what you will do in closure conversion!

Why not put them in global variable? [local-2]

You might pop up an idea, what if we reuse global variables for captured variables? For example, you might write the below code.

int c;

int bar() { return c + 1; }
typedef int (*bar_t)();

bar_t foo(int n) {
  c = n;
  return &bar;
}

int main() {
  bar_t b1 = foo(1);
  bar_t b2 = foo(2);
  printf("%d\n", b1());
  printf("%d\n", b2());
}

However, this produces 3 3 rather than 2 3, this is because foo(1) and foo(2) affect the same c! Another way to say this is that you're using the same environment for different calls, or you have at most one closure in this encoding.

Lambda lifting [local-3]

A rough idea is not storing variables somewhere, but extending all lambda's parameters, as much as captured variables it used! This process is lambda lifting. The idea is simple, let's have an example to show.

(lambda (x y z)
(let ((f (lambda (a b)
           (+ (* a x) (* b y)))))
(- (f 1 2) (f 3 4))))

goes to

(lambda (x y z)
(let ((f (lambda (x y a b)
           (+ (* a x) (* b y)))))
(- (f x y 1 2) (f x y 3 4))))

But this approach cannot handle a returned lambda! Since

  1. Stack will be destroyed, just like what happened in C
  2. The caller cannot sure how to give this captured variable to this returned lambda, because both are in its callee. Of course, for some simple languages global analysis should work, but in a language with side effects? I don't believe things can be that easy.

Thus, you should already be convinced that closure conversion is needed, let's take a look at it.

Closure conversion [local-8]

The complete code for this section lives in still-compiling/closure-conversion, including the parser and the driver that runs the passes in order.

First of all, we need a simple target to track our understanding, here we go:

(begin
(define (make-adder n)
  (lambda (m)
    (+ m n)))
((make-adder 2) 3))

Then we need to define our range of language, let me show the nanopass

(define (primitive? x) (member x '(+ - * / cons car cdr vector vector-ref)))
(define-language L0
  (terminals
   (primitive (p))
   (symbol (x))
   (number (n)))
  (Expr (e body)
        x
        n
        p
        (begin body* ... body)
        (lambda (x* ...) body* ... body)
        (let ([x* e*] ...)
          body* ... body)
        (define (x x* ...)
          body* ... body)
        (define x e)
        (e e* ...)))

Step one: remove define lambda form [local-4]

Using nanopass's idea, you will simplify language step by step to get an easier language to handle, here the point is converting define lambda form from a special form to define form. After this step, there will leave only define form.

(define-language L1
  (extends L0)
  (Expr (e body)
        (- (define (x x* ...) body* ... body))))
(define-pass remove-define-lambda : L0 (e) -> L1 ()
  (Expr : Expr (e) -> Expr ()
        [(define (,x ,x* ...) ,[body*] ... ,[body])
         `(define ,x
            (lambda (,x* ...)
              ,body* ... ,body))]))

Step two: wrap all expressions in a begin form [local-5]

In this step, you make all expressions in different forms be wrapped by begin!

(define-language L2
  (extends L1)
  (Expr (e body)
        (- (lambda (x* ...) body* ... body)
           (let ([x* e*] ...)
             body* ... body))
        (+ (lambda (x* ...) body)
           (let ([x* e*] ...) body))))
(define-pass wrap-with-begin : L1 (e) -> L2 ()
  (definitions
    (define (wrap body* body)
      (if (empty? body*)
          body
          `(begin ,body* ... ,body))))
  (Expr : Expr (e) -> Expr ()
        [(lambda (,x* ...) ,[body*] ... ,[body])
         `(lambda (,x* ...) ,(wrap body* body))]
        [(let ([,x* ,[e*]] ...) ,[body*] ... ,[body])
         `(let ([,x* ,e*] ...) ,(wrap body* body))]))

Find free variables [local-6]

Now, you are ready to touch on the core idea, finding free variables.

(define-pass freevars : L2 (e) -> * ()
(Expr : Expr (e) -> * ()
      [,x (set x)]
      [(lambda (,x* ...) ,body)
       (set-subtract (freevars body) (list->set x*))]
      [(let ([,x* ,e*] ...) ,body)
       (apply set-union
              (cons (set-subtract (freevars body)
                                  (list->set x*))
                    (map freevars e*)))]
      [(begin ,body* ... ,body) (apply set-union (map freevars (cons body body*)))]
      [(,p ,e* ...) (apply set-union (map freevars e*))]
      [(,e ,e* ...) (apply set-union (map freevars (cons e e*)))]
      [(define ,x ,e) (freevars e)]
      [else (set)]))

Explanation

  1. variable is free
  2. bounds in let and lambda form will be removed from frees
    • lambda is quite simple, get freevars in body then remove parameters from that set
    • let have to count e* in too, but cannot eliminate bounds from them, because left-hand bounds in let didn't occur at right-hand expressions
    • but we can have let* and letrec, they would be much more complicate
  3. map this function to each expression in begin form
  4. map this function to application form
    • handle primitive calls especially, don't count function part as freevars
    • normal call count function part in
  5. recur on define form's expression part. Notice that this is not enough, see below.
  6. rest are ignored

That fifth point produces code that does not run, so it is worth spelling out. Take a body that defines a name and then captures it:

(begin (define (outer)
         (begin (define w 5)
                (lambda (u) (+ u w))))
       ((outer) 1))

w is counted as free in outer, because the define clause only recurs into the right-hand side and never removes x from the enclosing scope. So the conversion builds outer's own environment as (vector w), reading w at a point where it does not exist yet, and the result fails with w: undefined. Handling define properly means treating the rest of the body as a scope in which x is bound.

Step three: closure conversion [local-7]

In this step, you will find all lambdas and do the following to each

  1. add a variable env for them
  2. encode environment as vector
  3. replace free variables in expression with vector-ref to environment index
  4. encode closure as a pair of the converted lambda and the environment
(define-pass replace-free : L2 (e $env fvs) -> L2 ()
  (Expr : Expr (e) -> Expr ()
        [,x (guard (set-member? fvs x))
            `(vector-ref ,$env ,(index-of (set->list fvs) x))]))
(define-pass closure-conversion : L2 (e) -> L2 ()
  (Expr : Expr (e) -> Expr ()
        [(lambda (,x* ...) ,[body])
         (define $env (gensym 'env))
         (define fvs (freevars e))
         ; convert free-vars in body by using reference to $env
         (if (set-empty? fvs)
             `(cons (lambda (,x* ... ,$env) ,body)
                    (vector))
             `(cons (lambda (,x* ... ,$env)
                      ,(replace-free body $env fvs))
                    (vector ,(set->list fvs) ...)))]))

NOTE: This relies on the fact that set->list is stable.

Then you have to convert the call into two kinds of call:

  1. call to primitive
  2. call to closure: take lambda out and put to the head of call, take environment out and put to the end of the call
(define-pass closure-call : L2 (e) -> L2 ()
(Expr : Expr (e) -> Expr ()
      [(,p ,[e*] ...)
       `(,p ,e* ...)]
      [(,[e] ,[e*] ...)
       (define $clos (gensym 'clos))
       `(let ([,$clos ,e])
          ((car ,$clos) ,e* ... (cdr ,$clos)))]))

Conclusion [local-10]

Now, you already get a workable closure conversion, all lambda has no free variables now! Thus, you can go further to get assembly without the model barrier now.

Challenge [local-9]

Some challenges here are for you to have more fun!

  1. Some lambdas have no free variables before converting, can you eliminate these closure conversions? How to mark them? In module sense?
  2. To get assembly, you need to generate a name for converted lambda, for example, lambda51289. But then you might get (define f lambda51289) after conversion, can you eliminate these?

Algorithm Strictly positive check [tt-0000]

Why? [tt-0001]

strictly positive 是 data type 中對 constructor 的一種特殊要求形成的屬性,這是因為如果一個語言可以定義出不是 strictly positive 的 data type,就可以在 type as logic 中定義出任意邏輯,形成不一致系統。

現在我們知道為什麼我們想知道一個 data type 是不是 strictly positive 了!了解完 strictly positive 的必要性後,我們用例子來理解什麼是不一致的系統,第一個例子是 not-bad :

Example Not Bad [tt-0004]

data Bad
  bad : (Bad → Bottom) → Bad

notBad : Bad → Bottom
notBad (bad f) = f (bad f)

isBad : Bad
isBad = bad notBad

absurd : Bottom
absurd = notBad isBad

Bottom (⊥\bot) 本來應該是不可能有任何元素的,即不存在 x 滿足 x : Bottom 這個 judgement,但我們卻成功的建構出 notBad isBad : Bottom。如此一來我們的型別對應到的邏輯系統就有了缺陷。

Example Loop [tt-0005]

現在我們關心一下第二個例子 loop(修改自 Certified Programming with Dependent Types):

data Term
  abs : (Term → Term) → Term

app : Term → Term → Term
app (abs f) t = f t

w : Term
w = abs (λ x → app x x)

loop : Term
loop = app w w

loop 的計算永遠都不會結束,然而證明器用到的 dependent type theory 卻允許型別依賴 loop 這樣的值,因此就能寫出讓 type checker 無法停止的程式。換句話說,證明器仰賴的性質缺失。事實上 Term 跟 Bad 的問題就是違反了 strictly positive 的性質,或許也有人已經發現了兩者 constructor 型別的相似之處。接下來我們來看為什麼這樣的定義會製造出不一致邏輯。

原理 [tt-0002]

首先我們需要理解以下兩條規則

  1. B⊆B′B \subseteq B' 蘊含 (A⇒B)⊆(A⇒B′)(A \Rightarrow B) \subseteq (A \Rightarrow B')
  2. A⊆A′A \subseteq A' 蘊含 (A′⇒B)⊆(A⇒B′)(A' \Rightarrow B) \subseteq (A \Rightarrow B')

根據這兩條規則,我們說 arrow type A⇒BA \Rightarrow B

  1. contravariant in AA(或 AA varies negatively)
  2. covariant in BB(或 BB varies positively

所以我們稱 AA 為 negative position、BB 為 positive position

實作 [tt-0003]

而 strictly positive 就是說,inductive family DD 自己不可以出現在 negative position

在 Agda 的文件中也有解釋:任何 constructor 都形如

(y1:B1)→⋯→(ym:Bm)→D(y_1 : B_1) \to \dots \to (y_m : B_m) \to D

而每個 BiB_i 都可以不提到 DD 或是形如

(z1:C1)→⋯→(zm:Cm)→D(z_1 : C_1) \to \dots \to (z_m : C_m) \to D

CjC_j 不可以等於 DD

文章到這邊告一段落,也解決了我這一年來對證明器實作的最大疑問之一,進一步的解答可以參考 https://cs.stackexchange.com/questions/55646/strict-positivity/55674#55674

Hindley-Milner type system: Incrementally build way & Make new [tt-NSSH]

Hindley-Milner (HM) type system is a classical type system for lambda calculus with parametric polymorphism. Its most notable property is it can infer most types of a given program, without type annotations! This feature sounds cool, though it is not work well in practice, since we need annotation to help ourselves when reading :). I pick this system as a topic is because the HM type system probably is the easiest complete system with parametric polymorphism. It's a good start for understanding other more complex type systems, and it's important for gradual typing. But before we dig too deep into those ideas, let's start to understand HM, the point of this article.

Why? [local-1]

In earlier days Lisp didn't have a type system, as time pass, people start to want to (or need to) express program more precisely since cooperation and robustness. Then people start working for their needs. To express a list, introduce parametric polymorphism. parametric polymorphism sounds scared but doesn't, let's view an example:

(: list-length (All (A) (-> (Listof A) Integer)))

This syntax bind list-length to a type (All (A) (-> (Listof A) Integer)), or we would write list-length : (All (A) (-> (Listof A) Integer))you can see Racket use prefix operator, very Lisp style, not surprise XD. The syntax is not the most important thing, but it shows a very common thing in many different languages, to help you get what is parametric polymorphism. Now imagine, every time call list-length, must provide A as an argument: (list-length Number lst). People would get tired and say: I hate the static type system, it seems no surprise. That's the reason for type inference. With type inference, if lst : (Listof Number), A is Number, get type without type! The idea is so frustrating and makes people crazy to think about: Can we have a system, get all the type without type? The result is the HM type system.

Why Polymorphism [local-0]

In simply typed lambda calculus (STLC), asking e : T make sense because we already give e a type K, check T is K is all we need. All type in STLC is a T or T -> T and T will not from another type. This feature, however, makes inconvenience when writing a program, for example:

(define id
  (lambda (x) x))

Identity function can work well with any type, but now we have to provide infinite versions for it:

(: id-str (-> str str))
(define id-str (lambda (x) x))
(: id-int (-> int int))
(define id-int (lambda (x) x))
(: id-bool (-> bool bool))
(define id-bool (lambda (x) x))
(: id-int-to-int (-> (-> int int) (-> int int)))
(define id-int-to-int (lambda (x) x))

If I don't want it and still want types, then use polymorphism is the solution:

(: id (All (A) (-> A A)))
(define id
  (lambda (x) x))

I hope I convince you that, take your time to understand its detail of this system is valuable :).

Setup a project [local-2]

This section helps you get a project would be modified in the following part:

raco pkg new hindley-milner
cd hindley-milner
raco pkg install --auto

The language, the types, and the state [local-3]

I would show a small enough language can cooperate with HM type system and big enough to convince you this is useful. Here is the whole shape of the data up front, then the rest of the article explains why each piece has to be there.

Definition of language (create lang.rkt):

#lang typed/racket

(provide expr expr:int expr:bool expr:string expr:list expr:variable expr:lambda expr:application expr:let)

(struct expr [] #:transparent)
(struct expr:int expr [(v : Integer)] #:transparent)
(struct expr:bool expr [(v : Boolean)] #:transparent)
(struct expr:string expr [(v : String)] #:transparent)
(struct expr:list expr [(elems : (Listof expr))] #:transparent)
(struct expr:variable expr [(name : String)] #:transparent)
(struct expr:lambda expr
  [(param : (Listof String))
   (body : expr)]
  #:transparent)
(struct expr:application expr
  [(func : expr)
   (args : (Listof expr))]
  #:transparent)
(struct expr:let expr
  [(bindings : (Listof (Pair String expr)))
   (expr : expr)]
  #:transparent)

Definition of types (create typ.rkt):

#lang typed/racket

(provide typ typ:builtin typ:freevar typ:constructor typ:arrow typ:scheme
         set-typ:freevar-subst!)

(struct typ [] #:transparent)
;;; A freevar is a mutable ref cell. `subst` is what it has been solved to,
;;; `#f` means it is still open.
(struct typ:freevar typ
  [(index : Integer)
   (subst : (Option typ))]
  #:transparent
  #:mutable)
(struct typ:constructor typ
  [(name : String)
   (arg : (Listof typ))]
  #:transparent)
(struct typ:arrow typ
  [(from : typ)
   (to : typ)]
  #:transparent)
;;; A type scheme: forall vars . body
(struct typ:scheme typ
  [(vars : (Listof typ))
   (body : typ)]
  #:transparent)

(: typ:builtin (-> String typ:constructor))
(define (typ:builtin name)
  (typ:constructor name '()))

Four shapes of type. typ:constructor covers int, bool, string and list A alike, so typ:builtin is just the zero-argument case of it. typ:arrow is the function type. typ:freevar is the placeholder we hand out when a type is not known yet, and typ:scheme is what makes a binding polymorphic. The last two are the whole story of this article, and they are two different mechanisms, not one.

One helper before anything else, because the error messages need it (create pretty-print.rkt):

#lang typed/racket

(require "typ.rkt")

(provide pretty-print-typ)

(: pretty-print-typ (-> typ String))
(define (pretty-print-typ t)
  (match t
    ([typ:freevar idx subst]
     (if subst
         (pretty-print-typ subst)
         (format "?~a" idx)))
    ([typ:constructor name typ-args]
     (if (empty? typ-args)
         (format "~a" name)
         (let ([j (string-join (map
                                (lambda ([typ-arg : typ])
                                  (pretty-print-typ typ-arg))
                                typ-args) " ")])
           (if (string=? name "pair")
               (format "(~a)" j)
               (format "(~a ~a)" name j)))))
    ([typ:scheme vars body]
     (format "(forall (~a) ~a)"
             (string-join (map (lambda ([v : typ]) (pretty-print-typ v)) vars) " ")
             (pretty-print-typ body)))
    ([typ:arrow from to]
     (format "~a -> ~a" (pretty-print-typ from)
             (pretty-print-typ to)))))

Notice the typ:freevar case already follows subst when the cell has been solved.

Two pieces of state, and everything from here to the end of the inference goes into this one file, in the order it appears. Environment maps variable names to types and forms a chain of scopes, and Context carries the environment plus the counter that hands out fresh freevars (create semantic.rkt):

#lang typed/racket

(provide type/infer Context Context/new)

(require "lang.rkt"
         "typ.rkt"
         "pretty-print.rkt")

(struct Env
  [(parent : (Option Env))
   (type-env : (Mutable-HashTable String typ))]
  #:transparent
  #:mutable)
(: Env/new (->* () ((Option Env)) Env))
(define (Env/new [parent #f])
  (Env parent (make-hash '())))
;;; Env/lookup take variable name such as `x` to get a type from env
(: Env/lookup (-> Env String typ))
(define (Env/lookup env var-name)
  (: lookup-parent (-> typ))
  (define lookup-parent (lambda ()
                          (: parent (Option Env))
                          (define parent (Env-parent env))
                          (if parent
                              ; dispatch to parent if we have one
                              (Env/lookup parent var-name)
                              ; really fail if we have no parent environment
                              (raise (format "no variable named: `~a`" var-name)))))
  (let ([typ-env : (Mutable-HashTable String typ) (Env-type-env env)])
    (hash-ref typ-env var-name lookup-parent)))

(: Env/bind-var (-> Env String typ Void))
(define (Env/bind-var env var-name typ)
  (let ([env (Env-type-env env)])
    (if (hash-has-key? env var-name)
        (raise (format "redefined: `~a`" var-name))
        (hash-set! env var-name typ))))

(struct Context
  [(freevar-counter : Integer)
   (type-env : Env)]
  #:transparent
  #:mutable)
(: Context/new (-> Context))
(define (Context/new)
  (Context 0 (Env/new)))
(: Context/new-freevar! (-> Context typ))
(define (Context/new-freevar! ctx)
  (let ([cur-count (Context-freevar-counter ctx)])
    (set-Context-freevar-counter! ctx (+ 1 (Context-freevar-counter ctx)))
    (typ:freevar cur-count #f)))

Unification [local-4]

Inference needs exactly two tools. This one answers these two types must be the same, what does that force?; the next one answers this binding can be used at many types, how?. We build both before walking the language, then the walk is short.

Solving a freevar is writing into its cell:

(: subst! (-> typ typ Void))
(define (subst! v t)
  (match v
    ([typ:freevar _ _] (set-typ:freevar-subst! v t))
    (_ (void))))

And because the cell remembers, nothing may look at a freevar directly. Every comparison has to force it first, following the chain until it reaches something that is not a solved freevar:

(: force (-> typ typ))
(define (force t)
  (match t
    ([typ:freevar _ s] (if s (force s) t))
    (_ t)))

So occurs, unify and free-vars below all force on the way in.

Occurs check makes unification fail when we are about to solve V to a type T that contains V itself. Without it we would accept

(= ?0 (list ?0))

and ?0 would stand for (list (list (list ...))), an infinite type. So we check whether v occurs in t, forcing on the way in:

(: occurs (-> typ typ Boolean))
(define (occurs v* t*)
  (define v (force v*))
  (define t (force t*))
  (match (cons v t)
    ; same freevar means `v` occurs in `t`, then should be rejected
    ([cons v (typ:freevar _ _)] (eqv? v t))
    ; arrow and constructor both just keep check on type parameters
    ([cons v (typ:arrow t1 t2)] (or (occurs v t1) (occurs v t2)))
    ([cons v (typ:constructor _ type-params)]
     (foldl (lambda ([t : typ] [pre-bool : Boolean])
              (or pre-bool (occurs v t)))
            #f
            type-params))
    ; rest is fine
    (_ #f)))

occurs only reports; unify below is what raises on a positive result, and that is how (lambda (x) (x x)) gets rejected.

Now unify itself:

(: unify (-> typ typ Void))
(define (unify t1* t2*)
  (define t1 (force t1*))
  (define t2 (force t2*))
  (match (cons t1 t2)
    ([cons (typ:constructor a al) (typ:constructor b bl)]
     #:when (string=? a b)
     (for-each (lambda ((ae : typ) (be : typ))
                 (unify ae be))
               al bl))
    ([cons (typ:arrow p1 r1) (typ:arrow p2 r2)]
     (unify p1 p2)
     (unify r1 r2))
    ;;; freevar type is the only interesting case in `unify`
    ((and
      [cons _ (typ:freevar _ _)]
      [cons t v])
     (cond
       ;;; already the same cell: nothing to solve, and writing here would make
       ;;; the cell point at itself, which `force` would then loop on
       ([eqv? v t] (void))
       ([occurs v t]
        (raise (format "occurs check failed: ~a in ~a"
                       (pretty-print-typ v) (pretty-print-typ t))))
       (else (subst! v t)))
     (void))
    ([cons (typ:freevar _ _) t2] (unify t2 t1))
    (_ (raise (format "cannot unify type ~a and ~a"
                      (pretty-print-typ t1) (pretty-print-typ t2))))))

Two constructors unify when the names match and their arguments unify pairwise. Two arrows unify componentwise --- in fact we could drop typ:arrow and encode it as a constructor named "->", all type constructors unify the same way. When one side is a freevar we solve it, after the occurs check. When only the left side is a freevar we flip and reuse the same case. Anything else is a type error.

Polymorphism: generalize and instantiate [local-5]

Binding a variable to its inferred type does not make it polymorphic. (lambda (a) a) infers to (?0) -> ?0, and ?0 is one cell. If id is bound to that type and looked up twice, both uses get the same cell, so the first use decides it forever.

Polymorphism needs the sharing to be broken. Two operations do it.

First, the freevars a type still has open. Force on the way in, so a cell already solved to int contributes nothing:

(: free-vars (-> typ (Listof typ)))
(define (free-vars t)
  (match (force t)
    ([typ:freevar _ _] (list (force t)))
    ([typ:arrow f to] (append (free-vars f) (free-vars to)))
    ([typ:constructor _ args]
     (append* (map (lambda ([a : typ]) (free-vars a)) args)))
    ;;; the ones bound by forall are not free
    ([typ:scheme vars body]
     (filter (lambda ([v : typ]) (not (memq v vars))) (free-vars body)))
    (_ '())))

(: env-free-vars (-> Env (Listof typ)))
(define (env-free-vars env)
  (append
   (append* (map (lambda ([t : typ]) (free-vars t))
                 (hash-values (Env-type-env env))))
   (let ([p (Env-parent env)])
     (if p (env-free-vars p) '()))))

memq and assq throughout: a freevar is a ref cell, so identity is the right comparison.

At the binding site, quantify the freevars of the type that do not also occur in the environment:

(: generalize (-> Env typ typ))
(define (generalize env t)
  (let* ([in-env (env-free-vars env)]
         [quantified (remove-duplicates
                      (filter (lambda ([v : typ]) (not (memq v in-env)))
                              (free-vars t)))])
    (if (empty? quantified)
        t
        (typ:scheme quantified t))))

Why exclude the environment's freevars? Consider

(lambda (x) (let ([y x]) ...))

x : ?0, and y gets ?0 as well. But ?0 is still reachable from the environment through x, and whoever calls this lambda will decide what it is. Quantifying it would make y usable at any type out of nothing, and

(lambda (x) (let ([y x]) '((y 1) (y "s"))))

would typecheck. It must not. So the rule is: quantify what belongs to this definition alone.

Note this also means generalize often quantifies nothing. If the body of a definition already pinned a variable down, there is no freevar left to quantify and the binding stays monomorphic, which is exactly right.

At every use site, replace each quantified variable with a brand new freevar and copy the type:

(: instantiate (-> Context typ typ))
(define (instantiate ctx t)
  (match t
    ([typ:scheme vars body]
     (let ([mapping : (Listof (Pairof typ typ))
                    (map (lambda ([v : typ]) (cons v (Context/new-freevar! ctx))) vars)])
       (copy/subst mapping body)))
    (_ t)))

(: copy/subst (-> (Listof (Pairof typ typ)) typ typ))
(define (copy/subst mapping t)
  (let ([t (force t)])
    (match t
      ([typ:freevar _ _]
       (let ([hit (assq t mapping)])
         (if hit (cdr hit) t)))
      ([typ:arrow f to]
       (typ:arrow (copy/subst mapping f) (copy/subst mapping to)))
      ([typ:constructor name args]
       (typ:constructor name (map (lambda ([a : typ]) (copy/subst mapping a)) args)))
      (_ t))))

The copy is the point. The scheme stays untouched in the environment so the next use can copy it again, and the two uses end up with two unrelated cells.

Incrementally build up inference [local-6]

With both tools in hand the inference itself is a walk over the syntax, one clause at a time. It also shows how to get the ideas behind the rules, not just remember a snapshot.

Monomorphism, which means obviously and decidable, and it would be a good start:

([expr:int _] (typ:builtin "int"))
([expr:bool _] (typ:builtin "bool"))
([expr:string _] (typ:builtin "string"))

You can see why I say they are obviously even you never use static type, because this type is builtin in most language, inference their types is just by definition. 1 is int, #t is bool, "hello" is string. However, there has a common builtin type is not monomorphism, for example: list.

list A is a type when A is a type, so to infer the type of a list we also have to infer the type of its elements and check the rest follow the first one. The edge case is the empty list: no element to look at, so we hand out a fresh freevar as the placeholder.

([expr:list elems]
 (typ:constructor "list"
                  (list (if (empty? elems)
                            (Context/new-freevar! ctx)
                            ; use first element type as type of all elements
                            (let ([elem-typ (type/infer (car elems) ctx)])
                              ; check all elements follow first element type
                              (for-each (lambda ([elem : expr]) (unify elem-typ (type/infer elem ctx)))
                                        (cdr elems))
                              elem-typ)))))

This is where the first freevar of a program usually comes from, and where unify first gets used.

A variable's type comes out of the environment --- and if what we find is a scheme, this is the use site, so instantiate it:

([expr:variable name] (instantiate ctx (Env/lookup (Context-type-env ctx) name)))

instantiate returns anything that is not a scheme unchanged, so lambda parameters and other monomorphic bindings just pass through.

Lambda in the HM system has no annotation on its parameters, so each parameter is bound to a fresh freevar in a new environment. Since multiple parameters are valid in this language, I use a type constructor named pair to group them rather than extending the type definition. Then infer the body under that environment and produce the arrow type:

([expr:lambda params body]
 (let* ([outer-env : Env (Context-type-env ctx)]
        [lambda-env : Env (Env/new outer-env)]
        [param-types (typ:constructor
                      "pair"
                      (map (lambda ([param-name : String])
                             (let ([r (Context/new-freevar! ctx)])
                               (Env/bind-var lambda-env param-name r)
                               r)) params))])
   (set-Context-type-env! ctx lambda-env)
   (let ([body-typ (type/infer body ctx)])
     (set-Context-type-env! ctx outer-env)
     (typ:arrow param-types body-typ))))

The environment is saved and put back around the body. Context holds one mutable environment, so without the restore the parameter would stay visible to whatever is inferred next --- in ((lambda (x) x) x) the second x would resolve to the parameter instead of failing as an unbound variable.

A lambda parameter is never generalized. That is not an omission, it is the definition of the system: a parameter is one type decided by the caller, so it gets one cell. Which is precisely why let has to be its own case.

let is the binding form that generalizes. Infer the right hand side, generalize it against the current environment, then bind the resulting scheme:

([expr:let bindings exp]
 (let* ([outer-env : Env (Context-type-env ctx)]
        [let-env : Env (Env/new outer-env)])
   (for-each (lambda ([bind : (Pairof String expr)])
               (match bind
                 ([cons name init]
                  (Env/bind-var let-env name
                                (generalize outer-env (type/infer init ctx))))))
             bindings)
   (set-Context-type-env! ctx let-env)
   (let ([body-typ (type/infer exp ctx)])
     (set-Context-type-env! ctx outer-env)
     body-typ)))

This single call to generalize is the whole difference between let and lambda, and it is what "let polymorphism" names. It is also what keeps inference decidable: quantification only ever happens at a binding whose right hand side we have already inferred, never at an unknown parameter.

Application needs a few steps:

  1. For application (func args ...), infer the type of func.
  2. Infer the type of every argument.
  3. Assume the return type is a fresh freevar.
  4. Unify the function's type with (args ...) -> freevar.
  5. Return that freevar as the result.
([expr:application fn args]
 (let ([fn-typ (type/infer fn ctx)]
       [args-typ (map (lambda ((arg : expr)) (type/infer arg ctx)) args)]
       [fresh (Context/new-freevar! ctx)])
   (unify fn-typ (typ:arrow (typ:constructor "pair" args-typ) fresh))
   fresh))

We never check "is this an arrow type" ourselves --- unify does it, and it also solves the argument and result types on the way. If func turns out to be int, the constructor case fails and we get a type error for free.

Put together:

(: type/infer (->* (expr) (Context) typ))
(define (type/infer exp [ctx (Context/new)])
  (match exp
    ([expr:int _] (typ:builtin "int"))
    ([expr:bool _] (typ:builtin "bool"))
    ([expr:string _] (typ:builtin "string"))
    ([expr:list elems]
     (typ:constructor "list"
                      (list (if (empty? elems)
                                (Context/new-freevar! ctx)
                                (let ([elem-typ (type/infer (car elems) ctx)])
                                  (for-each (lambda ([elem : expr]) (unify elem-typ (type/infer elem ctx)))
                                            (cdr elems))
                                  elem-typ)))))
    ([expr:variable name] (instantiate ctx (Env/lookup (Context-type-env ctx) name)))
    ([expr:lambda params body]
     (let* ([outer-env : Env (Context-type-env ctx)]
            [lambda-env : Env (Env/new outer-env)]
            [param-types (typ:constructor
                          "pair"
                          (map (lambda ([param-name : String])
                                 (let ([r (Context/new-freevar! ctx)])
                                   (Env/bind-var lambda-env param-name r)
                                   r)) params))])
       (set-Context-type-env! ctx lambda-env)
       (let ([body-typ (type/infer body ctx)])
         (set-Context-type-env! ctx outer-env)
         (typ:arrow param-types body-typ))))
    ([expr:let bindings exp]
     (let* ([outer-env : Env (Context-type-env ctx)]
            [let-env : Env (Env/new outer-env)])
       (for-each (lambda ([bind : (Pairof String expr)])
                   (match bind
                     ([cons name init]
                      (Env/bind-var let-env name
                                    (generalize outer-env (type/infer init ctx))))))
                 bindings)
       (set-Context-type-env! ctx let-env)
       (let ([body-typ (type/infer exp ctx)])
         (set-Context-type-env! ctx outer-env)
         body-typ)))
    ([expr:application fn args]
     (let ([fn-typ (type/infer fn ctx)]
           [args-typ (map (lambda ((arg : expr)) (type/infer arg ctx)) args)]
           [fresh (Context/new-freevar! ctx)])
       (unify fn-typ (typ:arrow (typ:constructor "pair" args-typ) fresh))
       fresh))))

Polymorphic use across two sites, which is the whole point:

(let ([id (lambda (a) a)]) (let ([x (id 1)]) (id "s")))   =>  string
(let ([id (lambda (a) a)]) id)                            =>  (?1) -> ?1
(let ([id (lambda (a) a)]) '((id 1) (id 2)))              =>  (list int)

The second line is worth a look: the printed variable is ?1, not the ?0 the definition created. That is instantiate handing out a copy.

And the things that must still be rejected:

(lambda (x) (x x))                                 =>  occurs check failed: ?0 in (?0) -> ?1
((lambda (x) (x 1)) 2)                             =>  cannot unify type (int) -> ?1 and int
(let ([f (lambda (a) '(a 1))]) (f "s"))            =>  cannot unify type int and string
(lambda (x) (let ([y x]) '((y 1) (y "s"))))        =>  cannot unify type int and string

The third one is the check on generalize being conservative: the body of f already unified a with int, so there is no free variable left to quantify and f is honestly (int) -> (list int). The fourth is the check on it not over-quantifying.

A type scheme is not a forall [local-7]

Damas-Milner's grammar is stratified:

τ::=α∣τ→τ∣C  τ…σ::=τ∣∀α.  σ\begin{aligned} \tau &::= \alpha \mid \tau \to \tau \mid C \; \tau \ldots \\ \sigma &::= \tau \mid \forall \alpha . \; \sigma \end{aligned}

σ\sigma only ever appears in the environment, as the type of a let-bound variable. ∀\forall is not a type constructor: you cannot write (∀α.  α→α)→int(\forall \alpha . \; \alpha \to \alpha) \to \text{int}, because ∀\forall does not fit into τ\tau. So HM's ∀α.  τ\forall \alpha . \; \tau really is a schema in the original sense of the word --- the same sense as an axiom schema, a template that generates types. instantiate applies the template. It does not eliminate a quantifier, because there is no quantifier there to eliminate.

System F's ∀α.  τ\forall \alpha . \; \tau is a different object entirely: it lives inside τ\tau, it is a first-class type, and Λ\Lambda is a real abstraction. A dependent Π(A:Type).  A→A\Pi (A : \text{Type}) . \; A \to A likewise. The notation is similar and the reading out loud is similar; the syntactic status is not.

The difference shows up the moment polymorphism has to cross an arrow. Ask for a parameter that is used at two types:

(lambda (f) '((f 1) (f "s")))

This fails with cannot unify type int and string. f is a lambda parameter, and a lambda parameter is bound to one freevar, so the two uses fight over the same cell. Nothing here is a scheme, so there is nothing to instantiate.

The next one is the interesting case, because a scheme really is involved:

(let ([id (lambda (a) a)])
  ((lambda (g) '((g 1) (g "s")))
   id))

Same error. id is a scheme sitting in the environment. But the moment it is passed as an argument it is instantiated to a monotype at the lookup, and g, being a lambda parameter again, could only ever hold a monotype anyway. Polymorphism cannot cross an arrow. That is what rank-1 (or prenex) means, and it is a direct consequence of σ\sigma not fitting into τ\tau --- not a shortcut in this implementation.

Is the restriction necessary? It is the price of the inference. With a first-class ∀\forall you need neither a separate category of schemes nor generalize at all: each application site elaborates its own metavariables, so the freshness comes from the site rather than from copying a template. What you give up is that you have to write the polymorphic type down --- typability for System F is undecidableReferenceTypability and type checking in the second-order λ-calculus are equivalent and undecidable1994 · J. B. Wells · 10.1109/lics.1994.316068, so there is no version of this article that infers a first-class ∀\forall for you. HM's stratification is exactly the trade that buys decidable inference.

Make new language in Racket [local-8]

After we build up a type system, we definitely want to see it work as a language, and make a new language in Racket is crazy easy.

To handle the whole module we overwrite #%module-begin, and we also want #%top-interaction for the REPL (modify main.rkt, which raco pkg new already made):

#lang racket

(require (for-syntax syntax/parse)
         racket/syntax
         syntax/stx)
(require "lang.rkt"
         "semantic.rkt"
         "pretty-print.rkt")

(provide (except-out (all-from-out racket) #%module-begin #%top-interaction)
         (rename-out [module-begin #%module-begin]
                     [top-interaction #%top-interaction]))

(define-syntax (parse stx)
  (define-syntax-class bind
    (pattern (bind-name:id bind-expr)
             #:with bind
             #'(cons (symbol->string 'bind-name) (parse bind-expr))))
  (syntax-parse stx
    [(_ ((~literal let) (binding*:bind ...) body))
     #'(expr:let (list binding*.bind ...) (parse body))]
    [(_ ((~literal lambda) (ps* ...) body))
     #'(expr:lambda (list (symbol->string 'ps*) ...) (parse body))]
    [(_ ((~literal quote) (elem* ...)))
     #'(expr:list (list (parse elem*) ...))]
    [(_ (f arg* ...))
     #'(expr:application (parse f) (list (parse arg*) ...))]
    [(_ v:id) #'(expr:variable (symbol->string 'v))]
    [(_ s:string) #'(expr:string (#%datum . s))]
    [(_ b:boolean) #'(expr:bool (#%datum . b))]
    [(_ i:exact-integer) #'(expr:int (#%datum . i))]))

(define-syntax-rule (module-begin EXPR ...)
  (#%module-begin
   (define all-form (list (parse EXPR) ...))
   (for-each (lambda (form)
               (printf "type:- ~a~n" (pretty-print-typ (type/infer form))))
             all-form)))

(define-syntax-rule (top-interaction . exp)
  (pretty-print-typ (type/infer (parse exp))))

(module reader syntax/module-reader
  hindley-milner)

parse is just mapping S expression onto the expressions defined in lang.rkt. Each clause matches on the shape of the form, and ~literal is what makes let and lambda keywords rather than variables. Note every pattern starts with (_ ...): the macro's own name is the first element of the syntax object.

With the reader submodule in place, #lang hindley-milner is a working language:

#lang hindley-milner

(let ([id (lambda (a) a)])
  (let ([x (id 1)])
    (id "s")))

(lambda (a) a)

(let ([id (lambda (a) a)]) '((id 1) (id 2)))

prints

type:- string
type:- (?0) -> ?0
type:- (list int)

and the REPL gives you a type per input:

> (let ([id (lambda (a) a)]) (id "s"))
"string"

Conclusion [local-9]

So the system is two mechanisms, not one. unify with a forced mutable ref solves the monotypes, and generalize/instantiate makes a let binding usable at several of them by handing out copies. The first half you would also want in a plain STLC with inference; the second half is what makes it Hindley-Milner, and what makes the inference decidable at the price of ∀\forall being prenex.

I hope detailed implementation and examples show why we need the HM system, and how to make one, and where we would need it. Have a nice day, and it's time for cookies!

Programming 生涯回顧 [2020]

很久以前就曾經想過要不要寫下這種經驗文,這念頭出現到現在轉眼就過了兩年了 XD。期間真的發生了太多事,現在又到了人生的十字路口所以決定記錄一些東西,希望這篇文章或許能夠啟發一些像我一樣迷惘的人。

大學教育 [local-0]

我接觸程式的時間不是特別早,到了大學才第一次”使用”電腦。那時真的蠻好笑的,我選了這裡的原因是因為我覺得這系名好長好有趣就填在指考志願第一個位置(而我當時也沒有試圖弄懂志願序的用途 XD),亂填的下場就是我爸差點沒氣死。當時幾經考慮,然而最後我還是決定不重考了。因為前進後或許不是一條好的道路,但到時候再後悔就好了,而不前進就必然要耗費一年也未必真的有什麼結果。到了學校,基礎程式課就立刻深深吸引了進了不知道能幹嘛的科系、對未來毫無想法的我。從此我每天從中午 12 點寫程式寫到半夜 3 點,每個禮拜只出現在程式課、書店、圖書館跟房間,搞到很多科都超低空飛過(雖然我也不在意)。印象特別深刻的是當初印出 99 乘法表的作業我硬是搞不懂 loop 的用途,死纏著學長問了 2 小時才寫出來。現在想想,真是佩服學長沒有從此不鳥我呢 XD。大一就在不太懂什麼檔案 IO、資料庫的情況下寫出了人生第一個能夠算是應用程式的程式:課堂的最終測驗,寫出一個運作在 CLI 下的 ATM(當時也不知道什麼是 CLI XD)。大二的資料結構與演算法課程中,小組討論促進了很多有趣的發想跟對演算法的理解(可惜整個科系就只有該門課程的教授這樣上課)。這門課讓我知道很多同學其實都能講出非常有見解的解法,即使他們後來未必會走上開發者這條道路。該課程結束後我跟學長(對,被我纏了 2 小時的那位)更常單獨討論他們當時的專題:為醫療領域最佳化的搜尋引擎。這個專題重點擺在如何針對特定領域最佳化搜尋結果,減少搜尋到農場文當然是非常重要的目標之一。由於當時對爬蟲的討論,我開始接觸 Erlang 與 Go 這兩個語言,接著幾個月都在不斷的改進寫好的爬蟲,讓它能夠在最短時間內處理最多的頁面。最後還不小心翻開龍書踏入了 Compiler 的奇妙世界。快樂的時光總是短暫,由於一些經濟因素,我決定休學走上工作的道路。

工作 [local-1]

第一份工作我靠著一個類 python 語言的直譯器說服了老闆為什麼雇用一個高中學歷的人也沒問題 www。現在回想這間公司真的很不錯,只要工作做完就完全不干涉你要做什麼,所以我在這裡寫了不少 Compiler 的程式,還把 redux porting 到 Go 裡面(雖然我到現在也沒弄懂這有什麼意義 XD)。當時朋友都在台北,也因為在家住膩了(結果現在又想回家住了),決定上台北找工作,就在台北待到了現在。剛上台北找工作時有一次很有趣的面試,當時我英文不好(連 Algorithm 都沒聽懂是什麼)、技能樹只往 Compiler 上點,面試官還願意一個一個跟我解釋我的問題在哪裏,最後還鼓勵我有學會的東西都記得很熟練,人真的超好 :),現在還是很感謝他。工作幾年下來,其實慢慢覺得工作帶來的成長不存在了,這也讓我有了想轉換人生跑道的想法,後面會再提。

認識 PLT [local-2]

在台北工作的兩年多裡慢慢開發著自己的程式語言,隨著得到更多的開發經驗和學習了更多的語言,我逐漸意識到我似乎有什麼根本性的東西根本沒有學到。就像當初我總覺得 Design Pattern 有點怪異、Go 開發者宣稱的簡單似乎不大對一樣的直覺,我發現了 PLT 這個以前從來沒有碰過的領域。從此開始入天坑:lambda calculus -> second order logic -> hindley-milner -> dependent type -> calculus of construction -> MLTT,現在正在讀 cubical type, inductive construction(CIC), HoTT 等。我認為學習 PLT 真的有很大的好處,會逐漸超出單個語言的見解,分解出是什麼讓程式變得好寫而什麼不是。也是對 PLT 的學習讓我終於弄懂為什麼 Go 的設計其實並沒有帶來簡單性、為什麼 Design Pattern 其實沒有必要背誦(甚至沒必要特意學習)。只是隨著了解的深入,我也更不想設計新的程式語言了 www(但又不想廢棄寫很久的專案)。但我不建議對上面的一切沒有興趣還硬讀,cs 有很多事情能做,絕對不只是 PLT。

避免崇拜 [local-3]

我覺得一路走來,最重要的教訓就是不要盲目的崇拜,不只是技術上,在生活中也應該檢視自己是不是做了什麼假設,而不是盲目的用被假設影響後的觀點看待事物。以我個人而言,崇拜過 Go, UNIX 等專案的作者和所謂的 hacker 就是該怎樣,進而導致了很多不良的選擇,例如:硬要用 vim 而不是 IDE、硬要用 CLI 而不是 GUI、明明有現成方案卻要從頭打造整個專案。這不是說用 vim 不對,很多時候我都用 vim。當環境上只有 terminal 或是只是要小改幾行時,我不否認它的編輯能力非常高效,但如果要說起跟 debugger 的整合性、移至定義的精準度、重構的方便度、閱讀其他人的程式碼等等,用 IDE 真的會好很多。關鍵是不要只因為某個看法就放棄對其他可能性的探索。對,就算我推薦 IDE,你還是該學看看 vim 或 emacs,說不準什麼時候就用到了。這也連結到怎麼選擇要學習什麼,只要避免崇拜,就較能以客觀的角度選擇合適的工具。

歸類知識 [local-4]

對知識進行分類是有好處的,記住生命週期長的資訊,而對有生命週期短的資訊不要耗費太多時間。例如說「理解演算法」就比「記住 vim 的 101 個指令」生命週期長的多。這裏,並不只是指事物本身的生命週期,要說起來 vim 活得超久,可能比很多演算法更久,但 vim 是容易過時的,你可能會遇到某些情況 vim 就是沒有 plugin 能用,這時候該做的恐怕不是自己刻個工具而是找替代方案,因為我們通常沒有時間,比起 vim 我更關心我想處理的問題,所以 agda 我用 atom 編輯,因為有方便的套件能用。而演算法就算不是最快的,其發想思路還是有相當的價值。同理對程式語言其實沒有必要花時間掌握語法到能夠「寫出最好的寫法」,因為我們關心的是程式碼解決的什麼問題,不是用什麼程式語言。關鍵是不要為了「非核心」的知識耗費大量心力,若這個「非核心」的知識不是你的興趣也不是你的工作職責。沒錯,如果就是喜歡玩 vim,開發 vim 的 plugin 就是樂趣根源那就去做吧,人類社會之所以能夠運作就是因為每個人是不一樣的。就像 PLT 知識對目標是寫個作業系統的人而言只是「非核心」知識一樣,每個人需要學習的知識都不一樣,所以也沒必要羨慕什麼「大神」,說不準人家還羨慕你能睡好覺,who knows。

下一個階段 [local-5]

雖然我被我講得好像我已經沒有迷惘了似地 XD,但完全不是這樣,我也在考慮去考在職碩士然後出國讀博士走學術這樣的路。人生沒有所謂的成功,有個笑話就說「窮人想有錢,有錢後被權貴欺負,就想著要有權,成官之後整天鬥爭,想想還是平淡的生活好,又成了窮人」,笑話是笑話,但人生沒有最佳解卻是事實,所以就算選了之後後悔也沒關係,需要的只是弄清楚自己接著想做什麼,然後去做。

世界上只有一種真正的英雄主義,那就是在認識生命的真相後,依然熱愛生活。

雖然我說的好像自己什麼都知道了,可惜就算是此時此刻我也搞不清楚我想幹嘛 w,但我還是會繼續學習我有興趣的知識、嘗試看看學術生涯是什麼樣子、體驗不同國家生活的風景。不知不覺就 23 歲,寫了 5 年程式,工作了 3 年,這世上唯一不缺的…大概就是時間不夠的感嘆,希望接下來 3 年也是有趣的人生。

人生不是只有程式碼 [local-6]

最後,人生不是只有程式碼,記得停下來看看那些幫助自己的人,我曾經沒有看清這麼簡單的事實。回憶過往,人生總是在受他人的幫忙,高中時帶給我人生樂趣的數學老師、嘴上抱怨但還是支持我的各種任性決定的父母、大學我整天吃吐司時請我吃飯的同學跟二話不說就借我幾千元的朋友、當初幫我跟老師求情不要當我的同學 www、工作時遇過的同事、在我低潮時聽我講抱怨的廢話的人們,只能再三感謝。這些才是讓我不用擔心,專心開發的支柱,如果有人讀完真的覺得自己沒有這樣的支柱,那也可以寄信給我,雖然我不見得能給什麼建議,保證沒辦法解決問題,但我可以好好聆聽,因為也有曾經願意聽我訴說的陌生人,雖然最後好像跟 programming 根本沒什麼關係 www,但還是希望這篇文章能夠幫助到一些人。話說回來,這部落格真的有讀者?

How to parse expression with the parser combinator [cs-2IWA]

Writing parser is a boring and massive job, therefore, people create many ways like parser generators to reduce costs. One of them called parser combinator, parser combinator solves two problems: 1. it is not as massive as a hand-crafted parser, 2. it is not as hard to debug as a parser generator. Well, it sounds perfect, doesn't it? A parser combinator is great, but one thing could be a problem: precedence descent parser. Unlike parser generator usually provided builtin supporting for operators' precedence. Parser combinator usually only provided basic support for sequence and selective parsing. You probably already heard about operator precedence parser, let's first take a look at its pseudo code(provided by Wiki):

parse_expression()
    return parse_expression_1(parse_primary(), 0)
parse_expression_1(lhs, min_precedence)
    lookahead := peek next token
    while lookahead is a binary operator whose precedence is >= min_precedence
        op := lookahead
        advance to next token
        rhs := parse_primary ()
        lookahead := peek next token
        while lookahead is a binary operator whose precedence is greater
                 than op's, or a right-associative operator
                 whose precedence is equal to op's
            rhs := parse_expression_1 (rhs, lookahead's precedence)
            lookahead := peek next token
        lhs := the result of applying op with operands lhs and rhs
    return lhs

Unfortunately, this is not suitable to port on to a parser based on combinator, because figure out how to introduce state monad into parser monad is a complex job, but that would not be a problem since we have a more intuitive solution which starts from avoiding the left recursion. If you have ever taken a compiler class and it, unfortunately, spend most of the time on parsing, then you may be heard left recursion:

A→AαA \rightarrow A \alpha

Where α\alpha is any sequence of terminal and non-terminal symbols. For example:

Expression→Expression+TermExpression \rightarrow Expression + Term

A naive implementation would loop on expression() forever. For example:

#lang racket

(define (expression)
  (expression)
  ; in Racket, a char `k` express as `#\k`
  (match #\+)
  (term))

To solve this problem, first, we break down syntax:

Factor→IntegerMultiplicativeExpr→Factor  (  ∗  Factor  )∗AdditiveExpr→MultiplicativeExpr  (  +  MultiplicativeExpr  )∗Factor \rightarrow Integer \\ MultiplicativeExpr \rightarrow Factor \; ( \; * \; Factor \; )^* \\ AdditiveExpr \rightarrow MultiplicativeExpr \; ( \; + \; MultiplicativeExpr \; )^*

Then we map them to parser combinators:

#lang racket

(require data/monad data/applicative)
(require megaparsack megaparsack/text)

(define lexeme/p
  ;;; lexeme would take at least one space or do nothing
  (do (or/p (many+/p space/p) void/p)
    (pure (λ () 'lexeme))))

(define (op/p op-list)
  (or/p (one-of/p op-list)
        void/p))
(define factor/p
  (do [expr <- integer/p]
    (lexeme/p)
    (pure expr)))
(define (binary/p high-level/p op-list)
  (do [e <- high-level/p]
    ; `es` parse operator then high-level unit, for example, `* 1`.
    ; therefore, this parser would stop when the operator is not expected(aka. operator is in op-list)
    ; rely on this fact we can leave this loop
    [es <- (many/p (do [op <- (op/p op-list)]
                     (lexeme/p)
                     [e <- high-level/p]
                     (pure (list op e))))]
    (pure (foldl
           (λ (op+rhs lhs)
             (match op+rhs
               [(list op rhs)
                (list op lhs rhs)]))
           e es))))
(define mul:div/p
  (binary/p factor/p '(#\* #\/)))
(define add:sub/p
  (binary/p mul:div/p '(#\+ #\-)))
(define expr/p add:sub/p)

Let's check its result: (parse-string expr/p "1 + 2 * 3 / 4 - 5") generate: (success '(#\- (#\+ 1 (#\/ (#\* 2 3) 4)) 5)) just as expected. Seems like expr/p is our target, but it still is a little massive, doesn't it? We have to modify the definition of each small parser once we need to insert more infix operators. To avoid this, finally going to the purpose of this article, we need an automatic way to do this for us. Observing the definition of parsers, there has a pattern: every infix operator layer can be a (binary/p high-level/p op-list). Using this fact we can create a recursive function:

(define (table/p base/p list-of-op-list)
  (if (empty? list-of-op-list)
      base/p
      (table/p (binary/p base/p (car list-of-op-list))
               (cdr list-of-op-list))))
(define expr/p
  (table/p factor/p
           '((#\* #\/)
             (#\+ #\-))))

The function takes a high-level parser and rest list of operator list. Use the head(car in Racket) of the list of operator list to create a layer parser via binary/p. If the list of operator list hasn't been empty, create more layers via table/p. Now we can handle infinite infix operators! Now, is time to take a break and have fun, have a nice day!

DPDK usertools: devbind [programming-IYOG]

After compiling DPDK, load module and start our process. A common problem is we have no idea where is the NIC going :).

And DPDK actually provides some tools for these operations, one of them is dpdk-devbind.py. It located at $(DPDK_PROJECT)/usertools/dpdk-devbind.py

We can use it to get current status:

$ dpdk-devbind.py --status
# shorthand
$ dpdk-devbind.py -s

Bind driver:

$ dpdk-devbind.py --bind e1000e 00:06.0
# shorthand
$ dpdk-devbind.py -b e1000e 00:06.0
# we also can use NIC name, but remember that a NIC could have no name
# only PCI would always existed.
$ dpdk-devbind.py -b igb_uio eth1

Unbind driver:

$ dpdk-devbind.py --unbind 00:06.0
# shorthand
$ dpdk-devbind.py -u 00:06.0
# equal to
$ dpdk-devbind.py --bind none 00:06.0

p.s. Remember that these operations requiring permission(sudo or what).

Just that, have fun!

DPDK -- EAL Input/output error [programming-S807]

Last week I'm trying to reproduce a bug happened in our customer environment, so we create a minimal example for this: https://github.com/glasnostic/nff_go_test

During this, I found an annoying problem and want to record it.

I got an error: EAL: Error enabling interrupts for fd 10 (Input/output error)

After some research, I found a patch for this(it didn't be merged into DPDK since it's a VMWare problem).

If you try to bind NIC that using e1000 you might have the same issue.

To solve this disables the checking by:

sed -i "s/pci_intx_mask_supported(dev)/pci_intx_mask_supported(dev)||1/g" \
  $(DPDK_PROJECT)/kernel/linux/igb_uio/igb_uio.c

This would make pci_intx_mask_supported check do not work anymore. Then recompile, after compiling done, reload the kernel module:

rmmod igb_uio
insmod $(DPDK_PROJECT)/build/kmod/igb_uio

p.s. DPDK_PROJECT is the project root directory of DPDK, related to your environment.

Then this problem should be fixed.

cgo can be a trouble [programming-S4P0]

This week, I have to upgrade nff-go from v0.7.0 to v0.8.1, so I change the version first. However, I found the whole package nff-go/low move to nff-go/internal/low, and our code directly based on low. After some research, I found all I need is call C function directly; the function is under librte_ethdev. So I add

// #include 
import "C"

into our code. I thought since nff-go already link DPDK, I have nothing to do here, but I'm wrong. The problem is that CGO would link C objects into temporary objects first. So linker would complain there was no reference to the function I call. So I add another line:

// #cgo LDFLAGS: -lrte_distributor -lrte_reorder -lrte_kni -lrte_pipeline -lrte_table -lrte_port -lrte_timer -lrte_jobstats -lrte_lpm -lrte_power -lrte_acl -lrte_meter -lrte_sched -lrte_vhost -lrte_ip_frag -lrte_cfgfile -Wl,--whole-archive -Wl,--start-group -lrte_kvargs -lrte_mbuf -lrte_hash -lrte_ethdev -lrte_mempool -lrte_ring -lrte_mempool_ring -lrte_eal -lrte_cmdline -lrte_net -lrte_bus_pci -lrte_pci -lrte_bus_vdev -lrte_timer -lrte_pmd_bond -lrte_pmd_vmxnet3_uio -lrte_pmd_virtio -lrte_pmd_cxgbe -lrte_pmd_enic -lrte_pmd_i40e -lrte_pmd_fm10k -lrte_pmd_ixgbe -lrte_pmd_e1000 -lrte_pmd_ena -lrte_pmd_ring -lrte_pmd_af_packet -lrte_pmd_null -Wl,--end-group -Wl,--no-whole-archive -lrt -lm -ldl -lnuma

They are directly copied from https://github.com/intel-go/nff-go/blob/v0.8.1/internal/low/low_no_mlx.go. However, linker unhappy with it, there are multiple definitions of symbols in the final object now, because we link two DPDK now.In pure CGO, we have no way to link only one at this situation.However, since we know the duplicate references are the same one so whatever the linker picks the program would work. Once we know that, we can use #cgo LDFLAGS: -Wl,--allow-multiple-definition to force the linker to ignore this duplicate. However, we won't want to have a copied from nff-go, so I did the trick in Makefile to copied it automatically when building.

How trait with lifetime can be a trouble and how to fix it [programming-B70A]

In my case, I have a trait called Resource for deserialize from bytes. Now I want to reuse a struct called List for others Resource so I write done:

struct List {
    // ignore others field
    items: Vec,
}

impl Resource for List {
    fn from_str(s: &str) -> Result> {
        let list: List = serde_json::from_str(s)?;
        Ok(list);
    }
}

Because serde_json::from_str requires impl Deserialize so we have to modify the code:

impl Resource for List {
    fn from_str(s: &str) -> Result> {
        let list: List = serde_json::from_str(s)?;
        Ok(list);
    }
}

It looks work but not. The problem is Deserialize actually is Deserialize<'de>, when we use the trait in the declaration we have to satisfy all type parameters of course includes lifetime. Ok, so we write:

impl<'a, T: Resource + Deserialize<'a>> Resource for List {
    fn from_str(s: &'a str) -> Result> {
        let list: List = serde_json::from_str(s)?;
        Ok(list);
    }
}

It looks good and works for most cases actually, however, in my code I hiding the whole get data and deserialize in a function, whatever it's, would cause a problem.

fn get_list_via_fetching_data<'a, T: Deserialize<'a>>() -> Result> {
    let data = fetch(); // in my case is kubernetes api server but that's fine
    List::::from_str(data)
}

Ok, you would find data is not live long enough for 'a, why? Since anyway 'a would be an outside lifetime and of course, live longer than anything in the function. What can we do for this case? We have to reverse the relationship between others Resource and List, rather than see List as kind of Resource, we add a function fn from_str_to_list(s: &str) -> Result>; for trait Resource. Since in my case Resource impl Sized and List also has a detected size after applying a Sized T so List is Sized. Now we can have the function get_list_via_fetching_data.

Of course, for the most normal case I think we don't need this one, and if at future '_ lifetime introduced the problem could be resolved(but I'm not sure that is for this problem actually so just a guess).

Thanks for reading and see you next time.

The Go concurrency bug I made [programming-S0L5]

There is a saying:

I never had a slice of bread particularly large and wide that did not fall upon the floor and always on the buttered side

Even I already work with Go for almost 3 years, I still made these stupid bugs. But learning from errors is why we are professional enigneer. So I'm going to list the bug I made, and show how to avoid it.

select with generator [local-0]

func events() <-chan int {
  ch := make(chan int)
  go func() {
      for {
          ch <- 1
      }
  }()
  return ch
}

func main() {
  for {
    select {
      case i := <- events():
        println("i=", i)
    }
  }
}

We using a common generator pattern here, and select also is quite normal case, the problem at here is select would call events not just once! This loop would create new channel for every case statement! And leaving infinite go-routine that nobody care!

To avoid the problem, you have to using range:

func main() {
  for i := range events() {
    // ...
  }
}

But if you want to stop this looping, which means you still need to use select, then store the channel to other place is required. There are many ways to do that:

  • In structure:

    type eventGenerator struct {
        eventCh chan int
        ctx     context.Context
        cancel  context.CancelFunc
    }
    
    func NewEventGenerator(ctx context.Context) *eventGenerator {
        // better to get context from others place, even this is a most up level controller
        // because you can use `context.Background()` as argument if this is the most up level one
        ctx, cancel := context.WithCancel(ctx)
        return &eventGenerator{
            // don't forget to `make` a channel,
            // if you skip it, Go won't give you any warning
            // And anything you try to send to it would be ignored!
            // No Warning!
            eventCh: make(chan int),
            ctx: ctx,
            cancel: cancel,
        }
    }
    
    func (e *eventGenerator) Start() {
        go func() {
            defer close(e.eventCh)
            for {
                select {
                case _, closed := <- e.ctx.Done():
                    if closed {
                        return
                    }
                default:
                    e.eventCh <- 1
                }
            }
        }()
    }
    func (e *eventGenerator) Events() <-chan int { return e.eventCh }
    func (e *eventGenerator) Close() { e.cancel() }

    Now you can write case <-eg.Events(): as you want after calling eg.Start() and stop it by eg.Close()

  • generator with outside channel

    func genEvents(ch chan int) {
        go func() {
            for {
                ch <- 1
            }
        }()
    }
    func main() {
        d := time.Now().Add(50 * time.Millisecond)
        ctx, cancel := context.WithDeadline(ctx, d)
        defer cancel()
        ch := make(chan int)
        genEvents(ch)
        for {
            select {
            case i := <-ch:
                println("i=", i)
            case <-ctx.Done():
                println("main:", ctx.Err().Error())
                close(ch)
                return
            }
        }
    }

2 misuse context.Done() [local-1]

Let's assuming there is a epoll like function call recv(), you would get something from it and deal with it, but it's not based on channel, which means you can't use it as case of select, how to deal with it?

func handlingRecv(ctx context.Context) <-chan interface{} {
    ch := make(chan interface{})
    go func() {
    defer close(ch)
        for {
            data := recv()
            var v interface{}
            err := json.Unmarshal(data, v)
            // ignore error handing
            ch <- v

            select {
            case _, closed := <-ctx.Done():
                return
            }
        }
    }()
    return ch
}

Code looks good? No, in fact the select would be blocked until this context be canceled, which means you can only get one message from recv(), and no warning, looks like a NICE networking problem, but it's a bug of code actually.

This bug is easy to fix, in fact, easier than previous one a lot.

select {
case _, closed := <-ctx.Done():
    return
default:
    // move job to here
}

So easy, we just based on the fact, if no case in, it would do default block work

Conculsion [local-2]

The bug show here might be not hard to solve, but since everything could go wrong would go wrong, I still wrote it done and if it's helpful that would be so great. Thanks for read.

Kubernetes Networking: concept and overview from underlying perspective [programming-RML7]

Kubernetes was built to run distributed systems on a cluster of nodes. Understanding the concept of kubernetes networking could help you correctly understand how to run, monitor and troubleshooting your applications on kubernetes, even more you can know how to choose a suitable distributed system by knowing how to compare them.

To understand it's networking configuration, we have to start from container and how the operating system provides these resource isolations. We start from network namespace this concept of Linux, and create a mock environment for learning how it works as container does. Now, let's begin!

Network namespace [local-3]

Before we start, my environment is Ubuntu 18.04 LTS, and here is the kernel information:

$ uname -a
Linux test-linux 4.15.0-1032-gcp #34-Ubuntu SMP Wed May 8 13:02:46 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

Create a new network namespace [local-0]

# create network namespace net0
$ ip netns add net0
# create network namespace net1
$ ip netns add net1
# then check
$ ip netns list
net1
net0 (id: 0)

Now we have several network namespaces could emit process on it, but the process can't connect to other networks is meaningless. To solve this problem, we have to create a tunnel for them, in Linux, we can use veth pair to connect two namespaces directly.

Connect two of them with a veth pair [local-1]

# new veth pair
$ ip link add type veth
# assign veth0 to net0
$ ip link set veth0 netns net0
# assign veth1 to net1
$ ip link set veth1 netns net1
$ ip netns exec net0 ip link set veth0 up
# assign ip 10.0.1.2 to veth0, you can use `ip addr` to check it
$ ip netns exec net0 ip addr add 10.0.1.2/24 dev veth0
$ ip netns exec net1 ip link set veth1 up
# assign ip 10.0.1.3 to veth1
$ ip netns exec net1 ip addr add 10.0.1.3/24 dev veth1

NOTE: An important thing is veth pair can't exist alone if you remove one, another would be removed.

Now, ping the network namespace net1 from net0

$ ip netns exec net0 ping 10.0.1.3 -c 3

tcpdump from target network namespace, of course, you should run tcpdump before you ping it.

$ ip netns exec net1 tcpdump -v -n -i veth1
tcpdump: listening on veth1, link-type EN10MB (Ethernet), capture size 262144 bytes
13:54:11.800223 IP6 (hlim 255, next-header ICMPv6 (58) payload length: 16) fe80::905d:ccff:fe4a:cd81 > ff02::2: [icmp6 sum ok] ICMP6, router solicitation, length 16
          source link-address option (1), length 8 (1): 92:5d:cc:4a:cd:81
13:54:12.400440 IP (tos 0x0, ttl 64, id 45855, offset 0, flags [DF], proto ICMP (1), length 84)
    10.0.1.2 > 10.0.1.3: ICMP echo request, id 1433, seq 1, length 64
13:54:12.400464 IP (tos 0x0, ttl 64, id 41348, offset 0, flags [none], proto ICMP (1), length 84)
    10.0.1.3 > 10.0.1.2: ICMP echo reply, id 1433, seq 1, length 64
13:54:13.464163 IP (tos 0x0, ttl 64, id 45912, offset 0, flags [DF], proto ICMP (1), length 84)
    10.0.1.2 > 10.0.1.3: ICMP echo request, id 1433, seq 2, length 64
13:54:13.464189 IP (tos 0x0, ttl 64, id 41712, offset 0, flags [none], proto ICMP (1), length 84)
    10.0.1.3 > 10.0.1.2: ICMP echo reply, id 1433, seq 2, length 64
13:54:14.488184 IP (tos 0x0, ttl 64, id 46671, offset 0, flags [DF], proto ICMP (1), length 84)
    10.0.1.2 > 10.0.1.3: ICMP echo request, id 1433, seq 3, length 64
13:54:14.488221 IP (tos 0x0, ttl 64, id 41738, offset 0, flags [none], proto ICMP (1), length 84)
    10.0.1.3 > 10.0.1.2: ICMP echo reply, id 1433, seq 3, length 64

HTTP can work also. HTTP server:

$ ip netns exec net1 python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 ...
# After you execute the following command here would show
10.0.1.2 - - [15/May/2019 13:55:41] "GET / HTTP/1.1" 200 -

HTTP client:

$ ip netns exec net0 curl 10.0.1.3:8000
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>Directory listing for /</title>
</head>
...

Although veth pair could help you connect two network namespaces, however, it can't work with more. While we are working on an environment with more than two network namespaces, we would need a more powerful technology: Bridge.

Connect more of them with a bridge [local-2]

# create bridge
$ ip link add br0 type bridge
$ ip link set dev br0 up
# create veth pair for net0, veth0 & veth1
$ ip link add type veth
# create veth pair for net1, veth2 & veth3
$ ip link add type veth
# set up veth pair of net0
$ ip link set dev veth0 netns net0
# You would find veth0 disappeared now by `ip link`
$ ip netns exec net0 ip link set dev veth0 name eth0
$ ip netns exec net0 ip addr add 10.0.1.2/24 dev eth0
$ ip netns exec net0 ip link set dev eth0 up
# bind veth pair of net0 to br0
$ ip link set dev veth1 master br0
$ ip link set dev veth1 up
# set up veth pair of net1
$ ip link set dev veth2 netns net1
$ ip netns exec net1 ip link set dev veth2 name eth0
$ ip netns exec net1 ip addr add 10.0.1.3/24 dev eth0
$ ip netns exec net1 ip link set dev eth0 up
# bind veth pair of net1 to br0
$ ip link set dev veth3 master br0
$ ip link set dev veth3 up

Now, ping 10.0.1.3 from net0 to check our bridge network.

$ ip netns exec net0 ping 10.0.1.3 -c 3
PING 10.0.1.3 (10.0.1.3) 56(84) bytes of data.
64 bytes from 10.0.1.3: icmp_seq=1 ttl=64 time=0.030 ms
64 bytes from 10.0.1.3: icmp_seq=2 ttl=64 time=0.059 ms
64 bytes from 10.0.1.3: icmp_seq=3 ttl=64 time=0.051 ms

--- 10.0.1.3 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2038ms
rtt min/avg/max/mdev = 0.030/0.046/0.059/0.014 ms

tcpdump our bridge: br0

$ tcpdump -v -n -i br0
tcpdump: listening on br0, link-type EN10MB (Ethernet), capture size 262144 bytes
12:43:39.619458 IP (tos 0x0, ttl 64, id 63269, offset 0, flags [DF], proto ICMP (1), length 84)
    10.0.1.2 > 10.0.1.3: ICMP echo request, id 3046, seq 1, length 64
12:43:39.619553 IP (tos 0x0, ttl 64, id 54235, offset 0, flags [none], proto ICMP (1), length 84)
    10.0.1.3 > 10.0.1.2: ICMP echo reply, id 3046, seq 1, length 64
12:43:40.635730 IP (tos 0x0, ttl 64, id 63459, offset 0, flags [DF], proto ICMP (1), length 84)
    10.0.1.2 > 10.0.1.3: ICMP echo request, id 3046, seq 2, length 64
12:43:40.635764 IP (tos 0x0, ttl 64, id 54318, offset 0, flags [none], proto ICMP (1), length 84)
    10.0.1.3 > 10.0.1.2: ICMP echo reply, id 3046, seq 2, length 64
12:43:41.659714 IP (tos 0x0, ttl 64, id 63548, offset 0, flags [DF], proto ICMP (1), length 84)
    10.0.1.2 > 10.0.1.3: ICMP echo request, id 3046, seq 3, length 64
12:43:41.659742 IP (tos 0x0, ttl 64, id 54462, offset 0, flags [none], proto ICMP (1), length 84)
    10.0.1.3 > 10.0.1.2: ICMP echo reply, id 3046, seq 3, length 64
12:43:44.859619 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 10.0.1.2 tell 10.0.1.3, length 28
12:43:44.859638 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 10.0.1.3 tell 10.0.1.2, length 28
12:43:44.859686 ARP, Ethernet (len 6), IPv4 (len 4), Reply 10.0.1.2 is-at 0a:e0:a1:07:b7:c9, length 28
12:43:44.859689 ARP, Ethernet (len 6), IPv4 (len 4), Reply 10.0.1.3 is-at d2:b6:de:2f:4e:f6, length 28

As you thought, br0 would get the traffic from net0 to net1, now we have topology looks like:

bridge_mode_and_namespace.svg

At the final of the output of tcpdump we can see some ARP request/reply, we would talk about it in the next section.

To get more info:

ARP [local-4]

ARP(Address Resolution Protocol) is a communication protocol used for discovering the link layer address, such as a MAC address associated with a given internet layer address.

NOTE: In IPv6(Internet Protocol Version 6), the functionality of ARP provided by NDP(Neighbor Discovery Protocol).

We aren't going to show the whole packet layout of ARP, but mention the part we care in the case.

The working process is:

  1. send ARP request packet with source MAC and source IP and target IP to broadcast address
  2. the machine thought it has this target IP would send ARP reply packet contains it's MAC address
  3. the machine sends ARP request would cache the mapping of IP and MAC into ARP cache, so next time it doesn't have to send ARP request again.

NOTE: others endpoint would ignore non-interested ARP request

arp_request_and_reply.svg

At the previous section, we can see both sides send ARP request to get another IP's information.

To get more info:

Pod to Pod [local-7]

Pod is the unit of kubernetes, so the most basic networking is how to connect from PodA to PodB. We would get two situation:

  1. two Pod at the same Node
  2. two Pod at the different Node

Node is a VM or a machine owned by the kubernetes cluster.

The following discussion assumes a bridge-style setup: every Node runs a Linux bridge, and every Pod gets a veth pair with the host end attached to that bridge. That is the simplest arrangement and it is what most CNI plugins do, so the packet flow below carries over even when the plugin differs.

Pods on the same Node [local-5]

In this case it is just the first section again: the Pods hang off the same bridge, conventionally named cbr0, each through its own veth pair.

Since we already covered this in the first section, we do not spend more time here. The interesting part is how to let Pods on different Nodes reach each other, which is the next section.

pod_to_pod_at_the_same_node.svg

Pods on different Nodes [local-6]

When containers have to talk across hosts the problem shows up. In the traditional model, any container that wanted to reach the outside world had to borrow a port on its host. In a cluster that does not scale, because ports are a limited resource. That is why we have CNM and CNI. We are not going to discuss their details, only the packet flow they have to produce.

Two Pods on different Nodes cannot share a bridge, so the packet cannot simply be forwarded at layer 2.

concept_of_nodes.svg

The whole packet flow looks like:

  1. PodA sends an ARP request
  2. ARP fails, so bridge cbr0 sends the packet out the default route, the host's eth0
  3. routing sends the packet to the default gateway
  4. the default gateway sends it to the right host by CIDR, e.g. 10.0.2.101 to 10.10.0.3
  5. the Node owning PodB's CIDR decides the packet belongs to its own cbr0
  6. cbr0 finally hands the packet to PodB
pod_to_pod_at_different_node_via_default_gateway.svg

Pod to Service [local-11]

We show how to route traffic between Pods and their IP addresses. The model works good until we have to scale the Pod. To make Kubernetes be a great system, we need to have the ability to add/delete resource automatically, which is the main feature of Kubernetes, now problem comes, because we could remove the Pod, we couldn't trust it's IP, since the new Pod won't get the same IP mostly.

To solve the problem, Kubernetes provide an abstraction called Service. A Service had some selectors and some port mappings with a cluster IP, which means it would select Pods as it's backend by selector and to loadbalancing for them and forward packets by port mappings. So whatever how Pods been created or deleted, Service would find those Pods with labels matched selectors, and we only have to know the IP of Service than know all IPs of Pods.

Now, let's take a look at how it works.

iptables and netfilter [local-8]

Kubernetes relies on netfilter – the networking framework bulit-in to Linux.

To get more info about netfilter please take a look at:

iptables is one of userspace tools based on the netfilter providing a table-based system for defining rules for manipulating and transforming packets. In Kubernetes, kube-proxy controller would config iptables rules by watching the changes from API server. The rule monitoring the traffic to the cluster IP of Service and picking a IP from IPs of Pods then forwarding the traffic to the picked IP by updating the destination IP from the cluster IP to the picked IP. This rule would be updated by cluster IP changed, Pod ADDED, Pod DELETED. Which means loadbalancing already been done on the machine to take traffic directed to cluster IP to an actual IP of Pod.

pod_to_service.svg

After the destination IP be updated, the networking model would fall back to the Pod to Pod model.

You can get more details of iptables via:

Load balancing [local-9]

Now I would create some iptables rules to mock a Service for static IPs. Assuming we have three IPs are: 10.244.1.2, 10.244.1.3, 10.244.1.4 and a cluster IP: 10.0.0.2

Start with a single backend:

iptables -t nat -A PREROUTING \
    -p tcp -d 10.0.0.2 --dport 80 \
    -j DNAT --to-destination 10.244.1.2:8080

Reading it piece by piece: -t nat picks the nat table, -A PREROUTING appends to the PREROUTING chain, -p tcp -d 10.0.0.2 --dport 80 matches only TCP traffic aimed at the cluster IP on port 80, and -j DNAT --to-destination 10.244.1.2:8080 rewrites the destination to a Pod.

Unfortunately, we can't just apply this command on to each IPs we want to loadbalance, because the first rule would take all the jobs from others(but our work won't be, damn). That's why iptables provides a module called statistic can work with two different modes:

  • random: probability
  • nth: round robin algorithm

Note: loadbalancing only works during the connection phase of the TCP protocol. Once the connection has been established, the connection would be routed to the same server.

We only introduce round robin here, since it's quite easy to understand and we want to talk about loadbalancing than how loadbalancing works.

$ export CLUSTER_IP=10.0.0.2
$ export SERVICE_PORT=80
$ iptables \
  -A PREROUTING \
  -p tcp \
  -t nat -d $CLUSTER_IP \
  --dport $SERVICE_PORT \
  -m statistic --mode nth \
  --every 3 --packet 0 \
  -j DNAT \
  --to-destination 10.244.1.2:8080

$ iptables \
  -A PREROUTING \
  -p tcp \
  -t nat -d $CLUSTER_IP \
  --dport $SERVICE_PORT \
  -m statistic --mode nth \
  --every 2 --packet 0 \
  -j DNAT \
  --to-destination 10.244.1.3:8080

$ iptables \
  -A PREROUTING \
  -p tcp \
  -t nat -d $CLUSTER_IP \
  --dport $SERVICE_PORT \
  -j DNAT \
  --to-destination 10.244.1.4:8080

The return path [local-10]

The DNAT above only rewrites the destination. The source stays 10.244.1.2, so podB replies to 10.244.1.2 --- an address on its own subnet, which means the reply goes straight back across the bridge at layer 2 instead of being routed. Whether conntrack ever sees that reply decides whether the connection works at all, and that is controlled by one sysctl:

net.bridge.bridge-nf-call-iptables

With it set to 0, bridged frames skip netfilter completely. Watching podA's interface while it talks to the cluster IP:

10.244.1.2.47394 > 10.0.0.2.80:      Flags [S]
10.244.1.3.8080  > 10.244.1.2.47394: Flags [S.]
10.244.1.2.47394 > 10.244.1.3.8080:  Flags [R]

The SYN went to the cluster IP but the SYN-ACK came back from the Pod IP. podA has no socket matching that address pair, so it resets the connection and the request never completes.

The tempting fix is to rewrite the source on the way out:

iptables -t nat -A POSTROUTING \
    -p tcp -s 10.244.1.2 --sport 8080 \
    -j SNAT --to-source 10.0.0.2:80

It does not help. The reply is bridged, so POSTROUTING never sees it either --- with this rule in place the capture is unchanged, still SYN-ACK from the Pod IP followed by a reset. The rule is fixing the wrong layer.

Set the sysctl to 1 and bridged IPv4 traffic is handed to netfilter, which is all conntrack needs:

10.244.1.2.49076 > 10.0.0.2.80:      Flags [S]
10.0.0.2.80      > 10.244.1.2.49076: Flags [S.]
10.244.1.2.49076 > 10.0.0.2.80:      Flags [P.]  HTTP: GET / HTTP/1.1

The reply now arrives from the cluster IP, because NAT in netfilter is connection tracked: the translation is computed once, on the first packet, and conntrack applies the reverse of it to everything coming back. The conntrack entry records both directions:

tcp TIME_WAIT src=10.244.1.2 dst=10.0.0.2 sport=49076 dport=80
              src=10.244.1.3 dst=10.244.1.2 sport=8080 dport=49076 [ASSURED]

The second tuple is the reply it expects, and matching against it is what turns 10.244.1.3:8080 back into 10.0.0.2:80. No SNAT required. This is why a Kubernetes node is expected to have net.bridge.bridge-nf-call-iptables set to 1 --- without it, every Service whose backend Pod sits on the same bridge as its client is quietly broken.

To get more info about loadbalancing and NAT (network address translation):

Internet to Service [local-14]

Egress [local-12]

Egress is traffic from a Pod out to the internet, say a packet from a Pod to some external service: 10.244.1.10 -> 8.8.8.8.

This is not straightforward, because 8.8.8.8 has no idea who 10.244.1.10 is --- they are not on the same network. So we need a globally routable IP, and the rewriting is called masquerading. Assume we have 219.140.7.218: the goal is to turn 10.244.1.10 into 219.140.7.218 before the packet reaches 8.8.8.8, and to turn it back on the way home.

Which raises the next problem: if several Pods make outgoing requests at once, which one gets a given reply? The simple answer (there are other NAT schemes) is to allocate a port per connection. 10.244.1.10:8080 -> 8.8.8.8:53 is rewritten as 219.140.7.218:61234, and a concurrent 10.244.1.11:8080 -> 8.8.8.8:53 as 219.140.7.218:61235. The replies come back to different ports, so each can be rewritten back to the right Pod.

Ingress [local-13]

A load balancer is the easy case: it just provides an IP for your Service and does exactly what the internal cluster IP does --- rewrite the destination and send the packet to the right Pod.

An ingress controller is an application layer load balancer instead. It terminates the HTTP request itself and routes on the path:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hello-world-ingress
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "false"
    nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  ingressClassName: nginx
  rules:
    - http:
        paths:
          - path: /hello(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: hello-svc
                port:
                  number: 80
          - path: /world(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: world-svc
                port:
                  number: 80

The controller owns the root path / and dispatches by prefix: anything under /hello goes to hello-svc, anything under /world goes to world-svc. Note that the controller usually picks the backend Pod inside its own code rather than going through the Service's cluster IP, so the Service here mostly serves as the list of endpoints.

HugePages on Kubernetes [programming-RR5A]

What is Huge Page? [local-0]

When a process using some memory, CPU marking the RAM used by the process. For efficiently, CPU allocate by chunks by 4K bytes(default on many platforms). Those chunks called pages. Since process address space is virtual, CPU and OS must remember which page belongs to which process, more memory been used, more pages need to be managed. To avoid heavy scheduling about pages, most current CPU architectures support the bigger page than 4KB, on Linux, it named huge page.

Kubernetes [local-1]

We are in the containerization world now, kubernetes is an open-source container orchestration system for automating deployment, scaling, and management. But some application requiring the huge page ability, in our case, that's DPDK.

So can we use enable hugepages in kubernetes? [local-3]

The answer is yes, but we have to check the version of kubernetes first.

According to feature-gates description. If your kubernetes version >= 1.10, then HugePages default on, else you have to enable it by yourself.

Log in to your kubernetes node machine. Open and edit /etc/default/kubelet this file, find -—feature-gates= these texts, add some text to make it looks like -—feature-gates=HugePages=true.

p.s. If you want to add more than one feature, use , separate the option, e.g. --feature-gates"…,DynamicKubeletConfig=true"=

Save and close editor, now run the following commands:

# according to your environment, this is optional
$ systemctl daemon-reload
$ systemctl restart kubelet

p.s. You might need sudo before the command, also according to your environment

p.s. advanced kubelet config

The previous setting is for kubernetes 1.8 and 1.9, 1.7 and below do not support this feature.

Now lets into the next section, how to mount huge page on to node.

Commands are easy:

$ mkdir -p /mnt/huge
$ mount -t hugetlbfs nodev /mnt/huge
# 1024 is the total number of hugepages, you can using others value as you need
$ echo 1024 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages

p.s. As the previous note, you might need sudo to do those stuff, and read this to know by sudo echo might not work as what you think

Then use: cat /proc/meminfo | grep Huge to see current status, after these, systemctl restart kubelet again.

Now leave your node machine, and use: kubectl describe nodes to check hugepages is enable or not. You might see something like:

apiVersion: v1
kind: Node
metadata:
  name: node1
# ignore...
status:
  capacity:
    memory: 10Gi
    hugepages-2Mi: 1Gi
  allocatable:
    memory: 9Gi
    hugepages-2Mi: 1Gi
# ignore...

An example pod:

apiVersion: v1
kind: Pod
metadata:
  name: example
spec:
  containers:
    # use the image you like
    volumeMounts:
      - mountPath: /hugepages
        name: hugepage
    resources:
      requests:
        hugepages-2Mi: 1Gi
      limits:
        hugepages-2Mi: 1Gi
  volumes:
    - name: hugepage
      emptyDir:
        medium: HugePages

Could we setting hugepages in Pod? [local-2]

For now, this feature seems won't support to use Pod configure hugepages, according to the proposal description.

After researching, we finally run up our Router with DPDK(although have other issues) in Pod, and we found we might not be able to mount hugepages at Pod init time.

Conculsion [local-4]

After learning about how to enable the hugepages, you might not have the chance to use hugepages directly, but a lot of third-party software would use it, then you can get the benefit from the hugepages. Thanks for the reading.

戴德金分割與1為何等於0.9...(無限循環) [math-YTJO]

基本前提 [local-0]

所有有理數皆可表示為兩個整數的比值,形如 pq\frac{p}{q} ,而無理數就是不能的那些,但是這個定義太含糊了,以至於許多年來數學家不願承認無理數是數字 用 pq\frac{p}{q} 就能輕鬆證明 2\sqrt{2} 不是有理數,這裡就不贅述

有理數有兩個性質

  • 稠密性(任意兩個有理數之間必然有無窮多個有理數)
  • 不完備性(任意兩個有理數之間必然有無窮多個無理數,故而不能構成連續性,即不完備)

戴德金分割 [local-1]

戴德金分割定義了一種形式,即我們可以在任何一點將有理數(集合 QQ)分割為兩集合 AA, BB

  1. A≠∅A \ne \varnothing
  2. A≠QA \ne Q
  3. If x,y∈Qx, y \in Q, x<yx < y, and y∈Ay \in A, then x∈Ax ∈ A
  4. If x∈Ax \in A, ∃y∈A\exists y \in A such that y>xy > x

1 和 2 非常直觀,你不能切出個什麼都沒有的集合,1 就是把 AA 都切沒了,反之 2 就是把 BB 切到一個不剩 3 有點難理解,這是什麼意思呢?這是在說 AA 是向下關閉(closed downwards)的,小於 AA 中元素的任意有理數元都比然屬於 AA 4 則是說明了一種情況為 AA 沒有最大的有理數存在 e.g. A={x∣x∈Q,x<2}A = \{x | x \in Q, x < 2\} 這裡 xx 到 22 之間也會有無窮多有理數故沒有最大值

那麼我們可以推論出三種分割

  1. AA 有最大元, BB 沒有最小元(即剛好分割於有理數且該有理數屬於 AA)
  2. AA 沒有最大元,BB 有最小元(即剛好分割於有理數且該有理數屬於 BB)
  3. AA 與 BB 皆沒有最大最小元(即分割於無理數上)

其中可以得出第三種分割點即為無理數

證明 0.9‾0.\overline{\rm 9} 等於 11 [local-2]

首先我們用 0.9‾0.\overline{\rm 9} 分割出集合 AA, BB 用 1 分割出集合 CC, DD

顯而易見的是我們只要證明 A≡CA \equiv C 即可

  • 對所有 a∈A,a<0.9‾,a<1,a∈Ca \in A, a < 0.\overline{\rm 9}, a < 1, a \in C (利用3,0.9‾0.\overline{\rm 9} 無論如何不大於 11 故可推論 aa 至少小於 11)
  • 令 c∈C,c<1c \in C, c < 1,其中 cc 可表示為 pq\frac{p}{q},故 1−pq>01-\frac{p}{q} > 0,且 1−pq≤1−1q1-\frac{p}{q} \leq 1-\frac{1}{q},且必然可以找到 nn 符合形式 1q>(110)n\frac{1}{q} > (\frac{1}{10})^n, 那麼 1−pq≤1−1q<1−(110)n<0.9‾1-\frac{p}{q} \leq 1-\frac{1}{q} < 1-(\frac{1}{10})^n < 0.\overline{\rm 9},即證明 c∈Ac \in A

綜合即知 AA 就是 CC,即 0.9‾=10.\overline{\rm 9} = 1

fun networking: tcp close [programming-0002]

Recently we are working on a new feature is about filter packets by HTTP header for our router. This is the concept, we read the header by rules, rules are just some key/value pair. If key missing or value isn't matched, then we drop the packet.

When a connection end, we would remove this connection from allowing pass map.

Anyway, we found a bug that, we will set a packet with FIN flag as the end of the TCP connection then drop any following packets, but the entire connection end is a:FIN -> b:ACK -> b:FIN -> a:ACK (a/b is client/server here). Hence, except the first packet, following packets won't pass our router, then connection would never end until timeout, LOL.

Reflection in Go: create a stack[T] [programming-0001]

Do you know what can Go's package reflect do?

Whatever you had use it or not. Understanding it is not a bad thing.

A well known thing is Go don't have generic, I'm not going to tell you we have generic, I'm going to tell you some basic trick to have the result like generic.

Take a look on the type Stack

type Stack struct {
    stack  []interface{}
    limitT *reflect.Type
}

limitT is a *reflect.Type, the reason that it's a pointer to reflect.Type rather than reflect.Type is because of we may do not spec it.

We add the Stack by invoke WithT.

func (s *Stack) WithT(v interface{}) *Stack {
    t := reflect.TypeOf(v).Elem()
    s.limitT = &t
    return s
}

Why is reflect.TypeOf(v).Elem()? Because we can't really get an instance that type is an interface! Instead of that, we can get a type is pointer to an interface!

We have a common idiom is using (*SomeInterface)(nil) to get pointer to interface instance.

Now we know that user code can be

type AST interface {
    Gen() llvm.Value
}

// main
s := stack.New().WithT((*AST)(nil))

After we do that, user can't push a value do not implement AST. So, how we do that? We do a check at Push

func (s *Stack) Push(element interface{}) {
    if s.limitT != nil {
        if !reflect.TypeOf(element).Implements(*s.limitT) {
            panic(fmt.Sprintf("element must implement type: %s", *s.limitT))
        }
    }
    s.stack = append(s.stack, element)
}

If limitT is not nil, means we do not limit the type, just keep going on. But if we limit the type, we check that element implements limitT or not. If not, we panic the process. Now we have a stack can promise type safe at runtime.

Magic in redux-go v2.1: package rematch [programming-10WZ]

A few days ago, I release the redux-go v2.1. The purpose is: create reducer & action then manage relationships between them is pretty hard! Let's start from basic v2 store.

// package reducer
func Counter(state int, action string) int {
    switch action {
    case "INCREASE":
        return state + 1
    case "DECREASE":
        return state + 1
    default:
        return state
    }
}

// func main
store := store.New(reducer.Counter)
store.Dispatch("INCREASE")
store.StateOf(reducer.Counter)

When you got 30 reducers, each contains 3 actions, how to manage this complex? In the traditional way, we follow a restriction naming rule. For example:

// package reducer/counter
const (
    Increase = "REDUCER_COUNTER_INCREASE"
    Decrease = "REDUCER_COUNTER_DECREASE"
)

// package reducer
func Counter(state int, action string) int

// func main
store := store.New(reducer.Counter)
store.Dispatch(counter.Increase)
store.StateOf(reducer.Counter)

How to spread these actions is not important, the point is we manage them by handcraft! Handcraft cause unstable! That's why we need package Rematch. It creates a more native way to manage your reducer-action relationship.

// package reducer/todo
var Reducer *todoModel

func init() {
    Reducer = &todoModel{
        State: make([]Todo, 0),
    }
}

type Todo struct {
    Title string
    Done bool
}

type Model []Todo

type todoModel struct {
    rematch.Reducer
    State Model
}

func (todo *todoModel) AddTodo(state Model, title string) Model {
    return append(state, Todo{Title: title})
}

Now when we using it, the relationship became pretty obviously

// func main
store := store.New(todo.Reducer)
addTodo := todo.Reducer.Action(todo.Reducer.AddTodo)

store.Dispatch(addTodo.With("first todo"))
store.Dispatch(addTodo.With("second todo"))

store.StateOf(todo.Reducer)

It takes more code but also more restrictive than the manual way to create it. Now, let's take a look at what made these happened. First, we start from store.New(base on v2.1.1)

// package store
func New(reducers ...interface{}) *Store {
    newStore := &Store{
        reducers: make(map[uintptr]reflect.Value),
        state:    make(map[uintptr]reflect.Value),
    }
    // later
}

The first difference is Store.reducers because, with rematch, reducer's address can't mapping to state, I will explain it later.

// func store.New
for _, reducer := range reducers {
    r := reflect.ValueOf(reducer)
    checkReducer(r)

    if _, ok := newStore.state[r.Pointer()]; ok {
        panic("You can't put duplicated reducer into the same store!")
    }

    actualReducer, initState := getReducerAndInitState(r)

    newStore.reducers[r.Pointer()] = actualReducer
    newStore.state[r.Pointer()] = initState
}
return newStore

Let's check reducer.

// func checkReducer, adding part
if r.Kind() == reflect.Ptr {
    v := reflect.Indirect(r) // dereference from ptr
    if v.FieldByName("State").Kind() == reflect.Invalid {
        panic("Reducer structure must contains field[State]")
    }
}

We add checking Kind is Ptr, because of rematch.Reducer sends a pointer of it into the store! If we can't find field State, we say the reducer is invalid and panic(this is a protocol really missing, but only the writer has to worry about, the user only need to know they have to create this field). So we can promise we don't have to check these at the following flow. Then we check the state already exist or not in the store. If the answer is yes, we panic it. Final, we have to get initial state and actual reducer, why it called actual reducer? Because we can't really execute a structure! The reducer will execute in progress is another thing. It created by package rematch. So let's dig into getReducerAndInitState this function to understanding how it works and why we have to change the type of Store.reducers.

// func getReducerAndInitState
if r.Kind() == reflect.Ptr {
    v := reflect.Indirect(r) // dereference from ptr
    return r.MethodByName("InsideReducer").
        Call([]reflect.Value{r})[0],
        v.FieldByName("State")
}
return r, r.Call(
    []reflect.Value{
    // We just use their zero value for initialize
        reflect.Zero(r.Type().In(0)), // In index 0 is state
        reflect.Zero(r.Type().In(1)), // In index 1 is action
    },
    )[0] // 0 at here is because checkReducer promise that we will only receive one return

The same, Kind is Ptr means it's rematch.Reducer. Remember actualReducer, initState : getReducerAndInitState(r)= this line, we got (reducer, state) pair. Now, when we receive a rematch.Reducer, reducer produce by InsideReducer, where is it? We do not see it at any user's code, right? Because it's defined at package rematch, export it is because reflection can only take exported member! Else its original reducer(a normal function apply reducer required), we won't talk about it again, you can refer to design-of-redux-go-v2 to getting more information. Back to InsideReducer.

// package rematch
func (r Reducer) InsideReducer(v interface{}) func(interface{}, *action) interface{} {
    r.ms = r.methods(v)
    return func(state interface{}, action *action) interface{} {
        return r.ms[action.reducerName()].Call(
            []reflect.Value{
                reflect.ValueOf(state),
                reflect.ValueOf(action.payload()),
            },
        )[0].Interface()
    }
}

As you can see, it returns a normal reducer finally, then you can find it very depends on r.methods. What is that? Let's view its definition.

// package rematch
func (r Reducer) methods(v interface{}) map[string]reflect.Value {
    rv := reflect.ValueOf(v)
    rt := reflect.TypeOf(v)
    methods := make(map[string]reflect.Value)
    for i := 1; i < rt.NumMethod(); i++ {
        m := rt.Method(i) // rt.Method.Func return func with first argument as receiver
        mt := m.Type
        if mt.NumIn() == 3 &&
            mt.NumOut() == 1 &&
            mt.In(1) == mt.Out(0) {
            // rv.Method return func with now receiver
            methods[m.Name] = rv.Method(i)
        }
    }
    return methods
}

methods get user-defined rematcher(back to InsideReducer & getReducerAndInitState, you will find this passing flow), overviewing every method, if anything looks like an inside reducer, put it into method map. Now you could have several confused points.

  1. why using m.Name, not address
  2. why using mt.In(1), not mt.In(0)
  3. why NumIn() should be 3

First question's answer is, instance to method & type to method has the different address! It's not hard to understand when you know that there has no user-type in final machine code. We will create a table(or other things, not important) to represent user-type. But we can get the same name(type info will store it). Second's answer and third's are same, reflection type of structure's method Method return an underlying function of method. For example, we have a type K, K has a method foo(), there has no K.foo() in this world, we have foo(*K) actually, and that's what rt.Method(i) gave you! Finally, let's take a look at action. The last puzzle of this crazy tutorial.

// package rematch
type action struct {
    funcName string
    with     interface{}
}

This is how it looks like. We store method's name & payload named as with. We used Action to create our action.

// package rematch
func (r Reducer) Action(method interface{}) *action {
    return &action{
        funcName: getReducerName(method),
    }
}

Now, we're believing getReducerName work correctly first, and mention it later. As your expected, With just set up the payload.

// package rematch
func (a *action) With(payload interface{}) *action {
    a.with = payload
    return a
}

reducerName & payload used in InsideReducer, them don't need to explain, just return the thing that action kept.

// package rematch
func (a action) reducerName() string {
    return a.funcName
}

func (a action) payload() interface{} {
    return a.with
}

getReducerName is the fuzziest thing, but just like we had mentioned, a method is a function that first parameter is its receiver!

// package rematch
func getReducerName(r interface{}) string {
    fullName := runtime.FuncForPC(reflect.ValueOf(r).Pointer()).Name()
    // fullName's format is `package.function_name`
    // we don't want package part.
    // package is full path(GOPATH/src/package_part) to it
    // len-3 is because a method contains suffix `-fm`
    return fullName[strings.LastIndexByte(fullName, '.')+1 : len(fullName)-3]
}

But why is len(fullName)-3? The reason is that you can have Foo & Foo(*K) at the same time! The solution Go pick is suffixed all method by -fm! Now you know why we cut it. Because of the type of method.Name does not have this suffix, we want to map them, so we have to follow their rules. With these change, now we can work with a native relationship between reducer & action! And a nice sleep I guess?

Error is Value [programming-0000]

I think most Gopher had read error-handling-and-go. Has anyone had watched Go Lift? Let's getting start from Go Lift! The point of Go Lift is: Error is Value. Of course, we know this fact. Do you really understand what that means? In Go Lift, John Cinnamond mentions a trick about wrapping the error by command executor. For example, we create a connection to server:6666 by TCP.

conn := net.Dial("tcp", "server:6666")

Can we? Ah…, No! The correct code is:

conn, err := net.Dial("tcp", "server:6666")
if err != nil {
    panic(err)
}

Then we write something through the connection.

nBtye := conn.Write([]byte{`command`})

We want that, but the real code is:

nBtye, err := conn.Write([]byte{`command`})
if err != nil {
    panic(err)
}
// using nByte

Next, we read something from server:6666, so we create a reader.

reader := bufio.NewReader(conn)
response := reader.ReadString('\n')

No! We have to handle the error.

response, err := reader.ReadString('\n')
if err != nil {
    panic(err)
}
// using response

Howver, the thing hasn't ended yet if we have to rewrite the command if response tells us the command fail? If we are working for a server, we can't just panic? So Go Lift has a solution:

func newSafeConn(network, host string) *safeConn {
    conn, err := net.Dial(network, host)
    return &safeConn{
        err: err,
        conn: conn, // It's fine even conn is nil
    }
}

type safeConn struct {
    err error

    conn net.Conn
}

func (conn *safeConn) Write(bs []byte) {
    if conn.err != nil {
    // if contains error, do nothing
        return
    }
    _, err := conn.Write(bs)
    conn.err = err // update error
}

func (conn *safeConn) ReadString(delim byte) string {
    if conn.err != nil {
        return ""
    }
    reader := bufio.NewReader(conn.conn)
    response, err := reader.ReadString("\n")
    conn.err = err
    return response
}

Then usage will become

conn := newSafeConn("tcp", "server:6666")
conn.Write([]byte{`command`})
response := conn.ReadString('\n')

if conn.err != nil {
    panic(conn.err)
}
// else, do following logic

Can we do much more than this? Yes! We can have an error wrapper for executing the task.

type ErrorWrapper struct {
    err error
}

func (wrapper *ErrorWrapper) Then(task func() error) *ErrorWrapper {
    if wrapper.err == nil {
        wrapper.err = task()
    }
    return wrapper
}

Then you can put anything you want into it.

w := &ErrorWrapper{err: nil}
var conn net.Conn
w.Then(func() error {
    conn, err := net.Dial("tcp", "server:6666")
    return err
}).Then(func() error {
    _, err := conn.Write([]byte{`command`})
})
Wait! We need to send the connection to next task without an outer scope variable. But how to? Now let's get into reflect magic.

type ErrorWrapper struct {
    err         error
    prevReturns []reflect.Value
}

func NewErrorWrapper(vs ...interface{}) *ErrorWrapper {
    args := make([]reflect.Value, 0)
    for _, v := range vs {
        args = append(args, reflect.ValueOf(v))
    }
    return &ErrorWrapper{
        err:         nil,
        prevReturns: args,
    }
}

func (w *ErrorWrapper) Then(task interface{}) *ErrorWrapper {
    rTask := reflect.TypeOf(task)
    if rTask.NumOut() < 1 {
        panic("at least return error at the end")
    }
    if w.err == nil {
        lenOfReturn := rTask.NumOut()
        vTask := reflect.ValueOf(task)
        res := vTask.Call(w.prevReturns)
        if res[lenOfReturn-1].Interface() != nil {
            w.err = res[lenOfReturn-1].Interface().(error)
        }
        w.prevReturns = res[:lenOfReturn-1]
    }
    return w
}

func (w *ErrorWrapper) Final(catch func(error)) {
    if w.err != nil {
        catch(w.err)
    }
}

Now, we're coding like:

w := NewErrorWrapper("tcp", "server:6666")

w.Then(func(network, host string) (net.Conn, error) {
    conn, err := net.Dial(network, host)
    return conn, err
}).Then(func(conn net.Conn) error {
    _, err := conn.Write([]byte{`command`})
    return err
}).Final(func(e error) {
    panic(e)
})

Design of Redux-go v2 [programming-BDGQ]

Redux is a single flow state manager. I porting it from JS to Go at last year. However, there had one thing make me can't familiar with it, that is type of state! In Redux, we have store combined by many reducers. Then we dispatch action into store to updating our state. That means our state could be anything. In JS, we have a reducer like:

const counter = (state = 0, action) => {
  switch (action.type) {
    case "INC":
      return state + action.payload
    case "DEC":
      return state - action.payload
    default:
      return state
  }
}

It's look good, because we don't have type limit at here. In Redux-go v1, we have:

func counter(state interface{}, action action.Action) interface{} {
    if state == nil {
        return 0
    }
    switch action.Type {
    case "INC":
        return state.(int) + action.Args["payload"].(int)
    case "DEC":
        return state.(int) - action.Args["payload"].(int)
    default:
        return state
    }
}

Look at those assertions, of course it's safe because you should know which type are you using. So ugly, I decide to change them. Therefore, in v2, we have:

func counter(state int, payload int) int {
    return state + payload
}

Wait, what!!!? So I have to explain the magic behind it. First is how to get user wanted type of state. The answer is reflect package. How? Let's dig in v2/store function: New.

func New(reducers ...interface{}) *Store

As you see, we have to accept any type been a reducer at parameters part. Then let's see type: Store(only core part)

type Store struct {
    reducers []reflect.Value
    state    map[uintptr]reflect.Value
}

Yp, we store the reflection result that type is reflect.Value. Why? Because if we store interface{}, we have to call reflect.ValueOf each time we want to call it! That will become too slow, state will have an explanation later. So in the New body.

func New(reducers ...interface{}) *Store {
    // malloc a new store and point to it
    newStore := &Store{
        reducers: make([]reflect.Value, 0),
        state:    make(map[uintptr]reflect.Value),
    }
    // range all reducers, of course
    for _, reducer := range reducers {
        r := reflect.ValueOf(reducer)
        checkReducer(r)
        // Stop for while
    }
}

Ok, what is checkReducer? Let's take a look now!

func checkReducer(r reflect.Value) {
    // Ex. nil
    if r.Kind() == reflect.Invalid {
        panic("It's an invalid value")
    }

    // reducer :: (state, action) -> state

    // Missing state or action
    // Ex. func counter(s int) int
    if r.Type().NumIn() != 2 {
        panic("reducer should have state & action two parameter, not thing more")
    }
    // Return mutiple result, Redux won't know how to do with this
    // Ex. func counter(s int, p int) (int, error)
    if r.Type().NumOut() != 1 {
        panic("reducer should return state only")
    }
    // Return's type is not input type, Redux don't know how would you like to handle this
    // Ex. func counter(s int, p int) string
    if r.Type().In(0) != r.Type().Out(0) {
        panic("reducer should own state with the same type at anytime, if you want have variant value, please using interface")
    }
}

Now back to New

// ...
for _, reducer := range reducers {
    // ...
    checkReducer(r)
    newStore.reducers = append(newStore.reducers, r)

    newStore.state[r.Pointer()] = r.Call(
        []reflect.Value{
            reflect.Zero(r.Type().In(0)),
            reflect.Zero(r.Type().In(1)),
        },
    )[0]
}
return newStore
// ...

So that's how state work, using a address of reducer mapping its state.reflect.Value.Call this method allow you invoke a reflect.Value from a function. It's parameter types required by signature. It always return several refelct.Value, but because we just very sure we only reutrn one thing, so we can just extract index 0. Then is state, why I choose to using pointer but not function name this time? Thinking about this:

// pkg a
func Reducer(s int, p int) int
// pkg b
func Reducer(s int, p int) int
// pkg main
func main() {
    store := store.New(a.Reducer, b.Reducer)
}

Which one should we pick? Of course, we can try to left package name make it can be identified. But next is the really hard:

func main() {
    counter := func(s int, p int) int { return s + p }
    store := store.New(counter)
}

If you think counter name is counter, that is totally wrong, it's name is func1. So, I decide using function itself to get mapping state. That is new API: StateOf

func (s *Store) StateOf(reducer interface{}) interface{} {
    place := reflect.Valueof(reducer).Pointer()
    return s.state[place].Interface()
}

The point is reflect.Value.Interface, this method return the value it owns. The reason we return interface{} at here is because, we have no way to convert to user wanted type, and user is always know what them get actually. For convenience we let users use any type for their state, so they don't need to do state.(int) these assertions. Now, you just work like this:

func main() {
    counter := func(s int, payload int) int {
        return s + payload
    }
    store := store.New(counter)
    store.Dispatch(10)
    store.Dispatch(100)
    store.Dispatch(-30)
    fmt.Printf("%d\n", store.StateOf(counter)) // expected: 80
}

These are the biggest break through for v2.