ClojureScript

命名空間

ClojureScript 的 ns 形式在大多數用法中可以非常簡單且類似於 Clojure,尤其是最近添加到 ClojureScript 的功能。

但是,ns 形式底下有一組豐富的選項,您可能會在實際應用中遇到它們。本指南旨在逐步介紹這些選項並提供一些說明。

巨集

在 ClojureScript 中,巨集的處理方式與 Clojure 有些不同。特別是,ns 形式支援 :require-macros,以及一些簡化的語法糖和隱式載入行為,我們將在這裡介紹。

基本元素

為了舉例說明,假設我們在 src/foo/core.clj 中有以下內容

(ns foo.core)

(defmacro add
  [a b]
  `(+ ~a ~b))

為了在某些 ClojureScript 原始碼中使用此巨集,您可以使用 :require-macros 規格,如下所示

(ns bar.core
  (:require-macros [foo.core]))

(foo.core/add 2 3)

現在,:require-macros 的設計與 :require 非常相似。因此,例如,您可以直接將 add 符號引用到您的命名空間中

(ns bar.core
  (:require-macros [foo.core :refer [add]]))

(add 2 3)

上述的另一種選擇 (不常見,但為了完整性值得介紹) 是 :use-macros。在 ClojureScript 中,:require / :refer:use / :only 本質上是彼此的雙重形式。因此,上面的 ns 形式也可以寫成 (ns bar.core (:use-macros [foo.core :only [add]]))

您還可以設定一個命名空間別名 foo

(ns bar.core
  (:require-macros [foo.core :as foo]))

(foo/add 2 3)

語法糖

上面的範例都使用了 :require-macros 基本元素。

但是,通常您會使用來自同一個命名空間的程式庫,其中提供執行時程式碼 (函式和其他 def) 和巨集。

因此,繼續我們的範例,假設有一個 src/foo/core.cljs 檔案,其中包含

(ns foo.core)

(defn subtract
  [a b]
  (- a b))

現在,如果您想同時使用 add 和 subtract,您可以執行類似以下的操作

(ns bar.core
  (:require-macros [foo.core :refer [add]])
  (:require [foo.core :refer [subtract]]))

(add 2 3)
(subtract 7 4)

但是,有一點 ns 形式的語法糖,:refer-macros,可讓您改為寫成

(ns bar.core
  (:require [foo.core :refer [subtract] :refer-macros [add]]))

(add 2 3)
(subtract 7 4)

上面的 :refer-macros 實際上只是語法糖,它會由編譯器以演算法方式解糖成之前的形式。

同樣地,有一個 :include-macros 語法糖,您可以使用它來表示應該使用與執行時命名空間相同的規格來要求巨集命名空間。因此,例如,以下程式碼可以運作

(ns bar.core
  (:require [foo.core :as foo :include-macros true]))

(foo/add 2 3)
(foo/subtract 7 4)

以上會解糖為更冗長的基本形式

(ns bar.core
  (:require-macros [foo.core :as foo])
  (:require [foo.core :as foo]))

(foo/add 2 3)
(foo/subtract 7 4)

隱式語法糖

在執行時命名空間被要求的案例中,如果該命名空間內部需要它自己的巨集命名空間 (表示具有相同名稱的命名空間),則您會免費獲得 :include-macros 語法糖。如果您正在開發程式庫,建議使用此方法,如此使用者可以簡化他們的 require 形式。

為了說明這一點,假設我們的 src/foo/core.cljs 檔案看起來像這樣

(ns foo.core
  (:require-macros foo.core))

(defn subtract
  [a b]
  (- a b))

現在,您可以像這樣使用程式碼

(ns bar.core
  (:require [foo.core :as foo]))

(foo/add 2 3)
(foo/subtract 7 4)

隱式引用

那麼,這個看起來不錯的簡化呢?

(ns bar.core
  (:require [foo.core :refer [add subtract]))

(add 2 3)
(subtract 7 4)

在這種情況下,add 是一個巨集,而 subtract 是一個函式的事實會由編譯器自動處理,因此可以統一引用變數,並且 ns 形式看起來基本上與在 Clojure 中相同。

文件

如果您在 REPL 中需要快速參考上述主題,則 ns 特殊形式的文件字串可提供協助。語法糖形式稱為內聯巨集規格,而隱式語法糖稱為隱式巨集載入。文件字串中包含一個相當完整的解糖範例。在緊要關頭,(doc ns) 是您的好幫手。

requirerequire-macros 巨集

您可以使用 requirerequire-macros 將程式碼動態載入到您的 REPL 中。有趣的是,上述功能也適用於這些巨集。

這是一個實作細節,但它可以幫助您了解如何完成此操作:當您發出

(require-macros '[foo.core :as foo :refer [add]])

在 REPL 中,這會在內部轉換為類似以下內容的 ns 形式

(ns cljs.user
  (:require-macros [foo.core :as foo :refer [add]]))

而且,重要的是,當您使用 require 時,會使用類似的 ns 形式,並且它會受到上述所有解糖和推斷行為的影響。

clojure 命名空間別名

某些命名空間 (例如 clojure.stringclojure.set) 可用於 ClojureScript 中,即使這些命名空間中的第一個區段是 clojure。但其他命名空間 (例如 cljs.pprintcljs.test,以及現在的 cljs.spec) 則位於 cljs 下。

為什麼會有這種差異?理想情況下,應該沒有差異。但是,如果您查看 ClojureScript 中使用的 clojure.pprint 連接埠,它會涉及一個巨集命名空間。這就是問題所在。由於 JVM ClojureScript 編譯器使用 Clojure 執行,如果沒有將連接埠移至 cljs.pprint,則會發生命名空間衝突。簡而言之,clojure.pprint 命名空間已被佔用。

這樣做的結果是,我們必須記住在編寫 ClojureScript 時,針對某些命名空間使用 cljs.*。而且,如果您正在編寫可攜式程式碼,則需要使用讀取器條件式。

ns 形式有一個相對較新的簡化功能,您可以使用它:在可以對應到 cljs.* 命名空間的不存在 clojure.* 命名空間的情況下,您可以使用 clojure 來代替命名空間的第一個區段中的 cljs

一個簡單的範例

(ns foo.core
  (:require [clojure.test]))

可以改為使用

(ns foo.core
  (:require [cljs.test]))

如果您這樣做,ClojureScript 編譯器會先查看是否可以載入 clojure.test 命名空間。由於它不存在,因此會回退到載入 cljs.test

同時,會從 clojure.test 設定到 cljs.test 的別名,就像您已寫入

(ns foo.core
  (:require [cljs.test :as clojure.test]))

這很重要,因為它可讓您擁有限定符號的程式碼,例如 clojure.test/test-var

透過這個別名,以及在 :refer 規格中推斷巨集變數的能力 (請參閱上方的「隱式引用」),以下程式碼可以在 ClojureScript 中正常運作

(ns foo.core-test
  (:require [clojure.test :as test :refer [deftest is]]))

(deftest foo-test
  (is (= 3 4)))

(test/test-var #'foo-test)

更重要的是:這與您在 Clojure 中編寫的程式碼完全相同。無需讀取器條件式!

當然,這也適用於 require ClojureScript 巨集。因此,例如,您可以執行

(require '[clojure.spec :as s])

然後,(s/def ::even? (s/and number? even?)) 將可正常運作。這樣做的原因是 require 巨集是根據 ns 特殊形式實作的。

總結

希望這些詳細的範例有助於釐清 ns 解糖、推斷和別名的運作方式。總體意圖是簡化 ClojureScript ns 形式的使用方式,但是解開這些額外功能如何運作可以讓您在想要或需要了解實際情況時有更好的理解。

善用這些功能應該有助於大幅減少 ClojureScript 和 Clojure ns 形式之間的差異。

原始作者:Mike Fikes