Clojure

預先編譯和類別產生

Clojure 會將您載入的所有程式碼動態編譯成 JVM 位元組碼,但有時預先編譯 (AOT) 會比較有利。使用 AOT 編譯的一些原因是

  • 在沒有原始碼的情況下傳送您的應用程式

  • 加快應用程式啟動速度

  • 產生命名類別供 Java 使用

  • 建立不需要執行時間位元組碼產生和自訂類別載入器的應用程式

儘管 Java 有程式碼重新載入的限制,但 Clojure 編譯模型盡可能保留 Clojure 的動態特性。

  • 來源和類別檔路徑遵循 Java 類別路徑慣例。

  • 編譯的目標是命名空間

  • 每個檔案、fn 和 gen-class 都會產生一個 .class 檔案

  • 每個檔案都會產生一個同名的載入器類別,並附加 "__init"。

  • 載入器類別的靜態初始化程式會產生與載入其來源檔案相同的效果

    • 通常您不需要直接使用這些類別,因為 use、require 和 load 會在這些類別和較新的來源中進行選擇

  • 當命名空間被編譯時,會為每個引用的檔案產生載入器類別,如果其載入器 .class 檔案比其來源舊。

  • 提供一個獨立的 gen-class 設施,用於建立命名類別,以直接作為 Java 類別使用,並提供以下功能

    • 命名產生的類別

    • 選擇超類別

    • 指定任何已實作的介面

    • 指定建構函數簽章

    • 指定狀態

    • 宣告其他方法

    • 產生靜態工廠方法

    • 產生 main

    • 控制對應到實作命名空間的對應

    • 公開繼承的受保護成員

    • 從單一檔案產生多個命名類別,並在一個或多個命名空間中實作

  • 可以在 ns 宣告中使用一個選用的 :gen-class 指令,以產生對應到命名空間的命名類別。如果提供 (:gen-class …​),預設值為對應到 ns 名稱的 :name、:main true、:impl-ns 與 ns 相同,以及 :init-impl-ns true。支援 gen-class 的所有選項。

  • 在未編譯時,會忽略 gen-class 和 :gen-class 指令。

  • 提供一個獨立的 gen-interface 設施,用於產生命名介面類別,以直接作為 Java 介面使用,並提供以下功能

    • 命名產生的介面

    • 指定任何超介面

    • 宣告介面方法的簽章

編譯

若要編譯函式庫,請使用 compile 函式,並提供命名空間名稱作為符號。對於在類別路徑中定義的某些命名空間 my.domain.lib,在 my/domain/lib.clj 中,應該會發生下列情況

  • 載入器類別檔會產生在 my/domain/lib__init.class 中,在 *compile-path* 下,它必須在類別路徑中

  • 將產生一組類別檔,每個類別檔對應命名空間中的一個函數,檔名如 `my/domain/lib$fnname__1234.class`

  • 對於每個 gen-class

    • 將產生一個指定名稱的 stub 類別檔

編譯器選項

Clojure 編譯器可透過使用多個編譯器標記來控制。在執行階段,這些標記儲存在動態變數 `clojure.core/*compiler-options*` 中,這個變數是一個具有下列選用關鍵字鍵的地圖

  • :disable-locals-clearing (布林值)

  • :elide-meta (關鍵字向量)

  • :direct-linking (布林值)

這些編譯器選項可在呼叫 `compile` 函數的動態繫結中變更,以變更編譯器行為。

或者,編譯器選項也可以在啟動時透過 Java 系統屬性設定

  • -Dclojure.compiler.disable-locals-clearing=true

  • "-Dclojure.compiler.elide-meta=[:doc :file :line :added]"

  • -Dclojure.compiler.direct-linking=true

請參閱下方,以取得每個選項的更多資訊。

清除區域變數

預設情況下,Clojure 編譯器會產生代碼,急切地清除對區域繫結的 GC 參照。但是,當使用偵錯器時,區域變數會顯示為 null,這使得偵錯變得困難。設定 `disable-locals-clearing=true` 會防止清除區域變數。不建議在生產編譯中停用清除區域變數。

省略元資料

變數元資料(文件字串、檔案和程式碼行資訊等)會編譯成已編譯類別常數池中的字串。為了縮小類別大小並加快類別載入,可以省略元資料。此選項會採用一個應移除的元資料關鍵字向量,其中一些常見的關鍵字包括 `:doc`、`:file`、`:line` 和 `:added`。請注意,省略元資料可能會使某些功能無法運作(例如,如果文件字串已省略,`doc` 將無法傳回文件字串)。

直接連結

通常,呼叫函數會導致變數解除參照,以找到實作該函數的函數執行個體,然後呼叫該函數。透過變數的這種間接呼叫是 Clojure 提供動態執行階段環境的方法之一。但是,長期以來,已觀察到生產環境中的大多數函數呼叫都不會以這種方式重新定義,導致不必要的重新導向。

直接連結 可用來以函數的直接靜態呼叫取代這種間接呼叫。這將導致更快的變數呼叫。此外,編譯器可以從類別初始化中移除未使用的變數,而直接連結會讓更多變數變得未使用。這通常會縮小類別大小並加快啟動時間。

直接連結的一個後果是,已使用直接連結編譯的程式碼將不會看到變數重新定義(因為直接連結會避免解除變數參照)。標記為 `^:dynamic` 的變數永遠不會直接連結。如果您希望將變數標記為支援重新定義(但不是動態),請使用 `^:redef` 標記它,以避免直接連結。

從 Clojure 1.8 開始,Clojure 核心函式庫本身就是使用直接連結編譯的。

執行時期

Clojure 產生的類別高度動態。特別注意,gen-class 中未指定任何方法主體或其他實作細節 - 它只指定一個簽名,而它產生的類別只是一個存根。此存根類別將所有實作遞延到實作命名空間中定義的函數。在執行時期,對已產生類別的某個方法 foo 的呼叫會找到 var implementing.namespace/prefixfoo 的目前值並呼叫它。如果 var 未繫結或為 nil,它會呼叫超類別方法,或者如果為介面方法,則產生 UnsupportedOperationException。

gen-class 範例

在最簡單的情況下,提供一個空的 :gen-class,而已編譯的類別只有 main,它是由在命名空間中定義 -main 來實作的。檔案應儲存在 src/clojure/examples/hello.clj 中

(ns clojure.examples.hello
    (:gen-class))

(defn -main
  [greetee]
  (println (str "Hello " greetee "!")))

要編譯,請確保目標輸出目錄 classes 存在

mkdir classes

並建立一個描述您的類別路徑的 deps.edn 檔案

{:paths ["src" "classes"]}

然後編譯以產生類別,如下所示

$ clj
Clojure 1.10.1
user=> (compile 'clojure.examples.hello)
clojure.examples.hello

並且可以像一般的 Java 應用程式一樣執行(請務必包含輸出類別目錄)

java -cp `clj -Spath` clojure.examples.hello Fred
Hello Fred!

以下是同時使用更複雜的 :gen-class,以及對 gen-class 和 gen-interface 的獨立呼叫的範例。在這個情況下,我們建立我們打算建立其執行個體的類別。clojure.examples.instance 類別將實作 java.util.Iterator,一個特別麻煩的介面,在於它要求實作有狀態。此類別將在其建構函式中採用一個字串,並以傳遞字串中的字元的方式實作 Iterator 介面。:init 子句命名建構函式。:constructors 子句是建構函式簽名對應到超類別建構函式簽名的對應。在這個情況下,超類別預設為 Object,其建構函式不採用任何引數。此物件將有稱為 state 的狀態,以及一個 main,因此我們可以測試它。

:init 函式(在本例中為 -init)很特別,因為它們總是會傳回一個向量,其第一個元素是超類別建構函式的引數向量,由於我們的超類別沒有任何引數,因此這個向量是空的。向量的第二個元素是實例的狀態。由於我們必須變更狀態(而狀態總是最終的),因此我們會使用一個指向包含字串和目前索引的映射的 ref。

hasNext 和 next 是 Iterator 介面中方法的實作。雖然這些方法沒有任何引數,但實例方法的實作函式總是會傳入一個額外的第一個引數,對應於方法被呼叫的物件,這裡依慣例稱為「this」。請注意如何使用一般的 Java 欄位存取來取得狀態。

gen-interface 呼叫會建立一個名為 clojure.examples.IBar 的介面,其中有一個方法 bar。

獨立的 gen-class 呼叫會產生另一個名為 clojure.examples.impl 的類別,其實作命名空間預設為目前的命名空間。它實作 clojure.examples.IBar。:prefix 選項會導致方法的實作繫結到以「impl-」開頭的函式,而不是預設的「-」。:methods 選項會定義一個新方法 foo,它不存在於任何超類別/介面中。

請注意在 main 中如何建立類別的實例,以及使用一般的 Java 互通呼叫方法。從 Java 使用它也會很一般。

(ns clojure.examples.instance
    (:gen-class
     :implements [java.util.Iterator]
     :init init
     :constructors {[String] []}
     :state state))

(defn -init [s]
  [[] (ref {:s s :index 0})])

(defn -hasNext [this]
  (let [{:keys [s index]} @(.state this)]
    (< index (count s))))

(defn -next [this]
  (let [{:keys [s index]} @(.state this)
        ch (.charAt s index)]
    (dosync (alter (.state this) assoc :index (inc index)))
    ch))

(gen-interface
 :name clojure.examples.IBar
 :methods [[bar [] String]])

(gen-class
 :name clojure.examples.impl
 :implements [clojure.examples.IBar]
 :prefix "impl-"
 :methods [[foo [] String]])

(defn impl-foo [this]
  (str (class this)))

(defn impl-bar [this]
  (str "I " (if (instance? clojure.examples.IBar this)
              "am"
              "am not")
       " an IBar"))

(defn -main [s]
  (let [x (new clojure.examples.instance s)
        y (new clojure.examples.impl)]
    (while (.hasNext x)
      (println (.next x)))
    (println (.foo y))
    (println (.bar y))))

如上編譯

$ clj
Clojure 1.10.1
user=> (compile 'clojure.examples.instance)
clojure.examples.instance

並像一般的 Java 應用程式一樣執行

java -cp `clj -Spath` clojure.examples.instance asdf
a
s
d
f
class clojure.examples.impl
I am an IBar