ClojureScript 1.12.145: 為 Lisp 風格的 Web 世界帶來 Async/Await

ClojureScript 團隊已宣布發布 1.12.145 版本,引入了生態系統中最受期待的功能之一:對 asyncawait 的原生支持。這次更新標誌著 ClojureScript 開發者與現代 JavaScript API 互動方式的重大轉變,朝著與 ECMAScript 2016 標準更無縫的整合邁進。

多年來,ClojureScript 用戶一直依賴外部函式庫或複雜的模式來處理非同步操作。透過允許編譯器產生原生的 JavaScript async 函式,ClojureScript 現在提供了一種一等公民的方式來處理 Promises 和非同步流程,而無需額外的依賴負擔。

在 ClojureScript 中實作 Async 函式

新功能是透過將函式標記為 ^:async 來觸發的。當編譯器遇到此提示時,它會產生一個 JavaScript async 函式。在這些函式中,可以使用 await 特殊形式來暫停執行,直到 Promise 被解析(resolved)。

請考慮以下實作方式:

(refer-global :only '[Promise])

(defn ^:async foo [n]
  (let [x (await (Promise/resolve 10))
        y (let [y (await (Promise/resolve 20)]
            (inc y))
        ;; not async
        f (fn [] 20)]
    (+ n x y (f))))

此語法也延伸到了測試框架,讓開發者能更自然地編寫非同步測試:

(deftest ^:async defn-test
  (try
    (let [v (await (foo 10))]
      (is (= 61 v)))
    (let [v (await (apply foo [10]))]
      (is (= 61 v)))
    (catch :default _ (is false))))

互操作性的權衡:原生 vs. CSP

async/await 的引入在社群內引發了一場關於 Clojure 非同步程式設計哲學的健康辯論。長期以來,ClojureScript 的併發處理金標準一直是 core.async,它實作了通訊序列程序(Communicating Sequential Processes, CSP)。

一些社群成員認為 CSP 模型在管理複雜狀態和程序間的通訊方面更為優越。正如一位用戶所說,將非同步函式封裝在 CSP 中通常被視為「處理此問題的更好方式」,因為 Clojure 已經擁有一套強大的模式。其他人則質疑引入 JS 風格的 async 關鍵字是否真的是一種升級,或者是否會過於偏離將邏輯推入通道(channels)的 Clojure 核心哲學。

然而,這次變更的主要驅動力是互操作性。Clojure 調查顯示,非同步支持是 JS 互操作性所需增強功能清單中的首選。透過支持原生 async/await,ClojureScript 消除了對 Promesa 等函式庫的需求,以便與幾乎完全基於 Promise 的現代瀏覽器 API 和熱門 JavaScript 函式庫進行常見的互動。

克服技術障礙

實現此功能的過程並非一蹴而就。根據社群見解,先前嘗試實作 async/await 的嘗試都受到了以下因素的阻礙:

  1. 編譯器複雜度:實作此功能需要對 ClojureScript 編譯器進行深層變更。包括 shadow-cljs 創作者在內的知名社群成員,先前的嘗試都得出結論認為這是一項艱鉅的任務。
  2. 語言差異化:在維持與 Clojure (JVM 版本) 的一致性與為 JS 執行環境提供必要工具之間存在緊張關係。由於 awaitclojure.core 中已經是一個關鍵字,因此在 ClojureScript 中引入此特殊形式代表了一種經過計算的偏離,以滿足 Web 環境的特定需求。

結論

ClojureScript 1.12.145 不僅僅是一個語法更新;它是對現代 Web 開發需求的務實回應。雖然 core.async 的 CSP 模型對於複雜的併發處理仍是強大的工具,而原生 async/await 則為與 JavaScript 生態系統的互動提供了一條輕量級、無摩擦的途徑。對於構建現代前端的開發者來說,能夠編寫感覺像是目標平台原生的非同步代碼,對於生產力與互操作性來說是巨大的勝利。

Sources