Clojure

Deps 和 CLI 指南

Clojure 提供命令列工具,用於

  • 執行互動式 REPL (讀取-評估-列印迴圈)

  • 執行 Clojure 程式

  • 評估 Clojure 表達式

在以上所有情況中,你可能想使用其他 Clojure 和 Java 函式庫 (相依性或「deps」)。這些可能是你正在本地撰寫的函式庫、git 中的專案 (例如在 GitHub 上) 或通常是 Maven 生態系統中可用的函式庫,並由 Maven Central 或 Clojars 等中央儲存庫所主機。

在所有情況下,使用函式庫都涉及

  1. 指定你想要使用的函式庫,提供其名稱和其他方面,例如版本

  2. 從 git 或 maven 儲存庫取得 (一次) 到你的本機電腦

  3. 讓它在 JVM 類別路徑上可用,以便 Clojure 可以在 REPL 或程式執行時找到它

Clojure 工具為 (a) 指定語法和檔案 (deps.edn),給定這些工具,它們將自動處理 (b) 和 (c)。

請參閱 入門,以取得如何安裝工具的詳細資訊。在此,我們將示範如何開始。請參閱 Clojure CLIdeps.edn 以取得完整參考。請參閱 變更日誌 以取得版本資訊。

執行 REPL 和使用函式庫

下載並安裝工具後,你可以透過執行 clj 工具來啟動 REPL

$ clj
Clojure 1.11.2
user=>

進入 REPL 後,你可以輸入 Clojure 表達式並按 Enter 鍵來評估它們。輸入 Control-D 退出 REPL

user=> (+ 2 3)   # press the `enter` key after typing the expression to evaluate it
5                # result of expression
user=>           # type Ctrl-D here to exit the REPL (does not print)
$

有許多 Clojure 和 Java 函式庫可用,可存取你可能需要的任何功能。例如,考慮常用的 Clojure 函式庫 clojure.java-time,用於處理日期和時間。

若要使用此函式庫,你需要將它宣告為相依性,以便工具可以確保已下載它並將它新增到類別路徑。大多數專案中的自述檔會顯示要使用的名稱和版本。建立一個 deps.edn 檔案來宣告相依性

{:deps
 {clojure.java-time/clojure.java-time {:mvn/version "1.1.0"}}}

或者,如果你不知道版本,你可以使用 find-versions 工具,它會列出所有可用的座標,並按順序排列

$ clj -X:deps find-versions :lib clojure.java-time/clojure.java-time
...omitted
{:mvn/version "1.0.0"}
{:mvn/version "1.1.0"}

使用 clj 工具重新啟動 REPL

$ clj
Downloading: clojure/java-time/clojure.java-time/1.1.0/clojure.java-time-1.1.0.pom from clojars
Downloading: clojure/java-time/clojure.java-time/1.1.0/clojure.java-time-1.1.0.jar from clojars
Clojure 1.11.2
user=> (require '[java-time.api :as t])
nil
user=> (str (t/instant))
"2022-10-07T16:06:50.067221Z"

第一次使用相依性時,您會看到關於下載函式庫的訊息。檔案下載後(通常會下載到 ~/.m2~/.gitlibs),未來會重複使用。您可以使用相同的程序將其他函式庫新增到您的 deps.edn 檔案,並探索 Clojure 或 Java 函式庫。

撰寫程式

很快地,您會想要建立並儲存自己的程式碼,以使用這些函式庫。建立一個新目錄,並將此 deps.edn 複製到其中

$ mkdir hello-world
$ cp deps.edn hello-world
$ cd hello-world
$ mkdir src

預設情況下,clj 工具會在 src 目錄中尋找原始檔。建立 src/hello.clj

(ns hello
  (:require [java-time.api :as t]))

(defn time-str
  "Returns a string representation of a datetime in the local time zone."
  [instant]
  (t/format
    (t/with-zone (t/formatter "hh:mm a") (t/zone-id))
    instant))

(defn run [opts]
  (println "Hello world, the time is" (time-str (t/instant))))

使用 main

此程式有一個入口函式 run,可以使用 -X 透過 clj 執行。

$ clj -X hello/run
Hello world, the time is 12:19 PM

使用本機函式庫

您可能會決定將此應用程式的一部分移至函式庫。clj 工具使用本機座標來支援僅存在於您的本機磁碟中的專案。讓我們將此應用程式的 java-time 部分抽取到平行目錄 time-lib 中的函式庫中。最後的結構將類似於以下內容

├── time-lib
│   ├── deps.edn
│   └── src
│       └── hello_time.clj
└── hello-world
    ├── deps.edn
    └── src
        └── hello.clj

在 time-lib 中,使用您已有的 deps.edn 檔案的副本,並建立一個檔案 src/hello_time.clj

(ns hello-time
  (:require [java-time.api :as t]))

(defn now
  "Returns the current datetime"
  []
  (t/instant))

(defn time-str
  "Returns a string representation of a datetime in the local time zone."
  [instant]
  (t/format
    (t/with-zone (t/formatter "hh:mm a") (t/zone-id))
    instant))

更新 hello-world/src/hello.clj 中的應用程式,以改用您的函式庫

(ns hello
  (:require [hello-time :as ht]))

(defn run [opts]
  (println "Hello world, the time is" (ht/time-str (ht/now))))

修改 hello-world/deps.edn 以使用本機座標,該座標指向 time-lib 函式庫的根目錄(請務必更新您電腦的路徑)

{:deps
 {time-lib/time-lib {:local/root "../time-lib"}}}

然後,您可以透過執行應用程式,從 hello-world 目錄測試所有內容

$ clj -X hello/run
Hello world, the time is 12:22 PM

使用 git 函式庫

與他人分享該函式庫會很棒。您可以透過將專案推送到公開或私人 git 儲存庫,並讓其他人使用 git 相依性座標來達成此目的。

首先,為 time-lib 建立一個 git 函式庫

cd ../time-lib
git init
git add deps.edn src
git commit -m 'init'

然後前往公開的 git 儲存庫主機(例如 GitHub),並遵循指示建立和發布此 git 儲存庫。

我們還想要標記此版本,以便它具有有意義的版本

git tag -a 'v0.0.1' -m 'initial release'
git push --tags

最後,修改您的應用程式以改用 git 相依性。您需要收集以下資訊

  • 儲存庫函式庫 - Clojure CLI 使用一個慣例,其中如果使用類似於 io.github.yourname/time-lib 的函式庫名稱,則不需要指定 GitHub URL https://github.com/yourname/time-lib.git

  • 標籤 - v0.0.1 是我們在上面建立的

  • sha - 標籤中的簡短 sha,如果你有本機儲存庫,請使用 git rev-parse --short v0.0.1^{commit} 尋找它,或者如果你有遠端儲存庫,請使用 git ls-remote https://github.com/yourname/time-lib.git v0.0.1 尋找它。你也可以使用 GitHub 儲存庫查看標籤及其備份提交來尋找它。

更新 hello-world/deps.edn 以改用 git 座標

{:deps
 {io.github.yourname/time-lib {:git/tag "v0.0.1" :git/sha "4c4a34d"}}}

現在你可以再次執行應用程式,使用(共用)git 儲存庫函式庫。你第一次執行它時,當 clj 下載並快取儲存庫和提交工作樹時,你會在主控台上看到額外的訊息

$ clj -X hello/run
Cloning: https://github.com/yourname/time-lib
Checking out: https://github.com/yourname/time-lib at 4c4a34d
Hello world, the time is 02:10 PM

現在你的朋友也可以使用 time-lib 了!

其他範例

隨著你的程式變得更複雜,你可能需要建立標準類別路徑的變體。Clojure 工具支援使用別名修改類別路徑,別名是 deps 檔案的一部分,只有在提供相應別名時才會使用。你可以執行的一些事情包括

包含測試來源目錄

通常,專案類別路徑預設只包含專案來源,而不包含其測試來源。你可以在類別路徑建構的 make-classpath 步驟中,將額外路徑新增為主要類別路徑的修改。為此,新增一個包含額外相對來源路徑 "test" 的別名 :test

{:deps
 {org.clojure/core.async {:mvn/version "1.3.610"}}

 :aliases
 {:test {:extra-paths ["test"]}}}

套用類別路徑修改,並透過呼叫 clj -A:test -Spath 來檢查修改後的類別路徑

$ clj -A:test -Spath
test:
src:
/Users/me/.m2/repository/org/clojure/clojure/1.11.2/clojure-1.11.2.jar:
... same as before (split here for readability)

請注意,測試目錄現在包含在類別路徑中。

使用測試執行器執行所有測試

你可以擴充前一節中的 :test 別名,以包含 cognitect-labs test-runner,用於執行所有 clojure.test 測試

擴充 :test 別名

{:deps
 {org.clojure/core.async {:mvn/version "1.3.610"}}

 :aliases
 {:test {:extra-paths ["test"]
         :extra-deps {io.github.cognitect-labs/test-runner {:git/tag "v0.5.1" :git/sha "dfb30dd"}}
         :main-opts ["-m" "cognitect.test-runner"]
         :exec-fn cognitect.test-runner.api/test}}}

然後使用預設設定執行測試執行器(執行 test/ 目錄下 -test 命名空間中的所有測試)

clj -X:test

新增一個選用依賴項

deps.edn 檔案中的別名也可以用來新增會影響類別路徑的選用依賴項

{:aliases
 {:bench {:extra-deps {criterium/criterium {:mvn/version "0.4.4"}}}}}

這裡的 :bench 別名用於新增一個額外的依賴項,也就是 criterium 基準測試函式庫。

您可以透過新增 :bench 別名來修改相依性解析,將此相依性新增至您的 classpath:clj -A:bench

從命令列新增相依性

在不將函式庫新增至現有的 deps.edn 檔案或建立新檔案的情況下,對其進行實驗可能會有幫助。

$ clojure -Sdeps '{:deps {org.clojure/core.async {:mvn/version "1.5.648"}}}'
Clojure 1.11.2
user=> (require '[clojure.core.async :as a])
nil

請注意,由於跳脫規則,最好將設定資料用單引號包起來。

準備來源相依性函式庫

有些相依性在可以在 classpath 上使用之前,需要執行準備步驟。這些函式庫應在其 deps.edn 中說明此需求

{:paths ["src" "target/classes"]
 :deps/prep-lib {:alias :build
                 :fn compile
                 :ensure "target/classes"}}

包含頂層金鑰 :deps/prep-lib 會告訴 tools.deps classpath 建構,需要額外準備此函式庫,而且可以透過在 :build 別名中呼叫 compile 函式來執行。準備步驟完成後,它應該會建立路徑 "target/classes",而且可以檢查是否已完成。

您依賴此函式庫就像依賴任何其他基於來源的函式庫(可能是 git 或本機)一樣

{:deps {my/lib {:local/root "../needs-prep"}}}

如果您接著嘗試將該函式庫包含在您的 classpath 中,您會看到一個錯誤

$ clj
Error building classpath. The following libs must be prepared before use: [my/lib]

然後您可以使用此命令告訴 CLI 進行準備(這是針對特定函式庫版本的一次性動作)

$ clj -X:deps prep
Prepping io.github.puredanger/cool-lib in /Users/me/demo/needs-prep
$ clj
Clojure 1.11.2
user=>

覆寫相依性

您可以同時使用多個別名。例如,此 deps.edn 檔案定義了兩個別名 - :old-async 用於強制使用較舊的 core.async 版本,而 :bench 用於新增額外的相依性

{:deps
 {org.clojure/core.async {:mvn/version "0.3.465"}}

 :aliases
 {:old-async {:override-deps {org.clojure/core.async {:mvn/version "0.3.426"}}}
  :bench {:extra-deps {criterium/criterium {:mvn/version "0.4.4"}}}}}

如下所示啟用兩個別名:clj -A:bench:old-async

包含磁碟上的本機 jar

偶爾您可能需要直接參照 Maven 儲存庫中不存在的磁碟 jar,例如資料庫驅動程式 jar。

使用指向 jar 檔案而非目錄的本機座標,指定本機 jar 相依性

{:deps
 {db/driver {:local/root "/path/to/db/driver.jar"}}}

先期(AOT)編譯

使用 gen-classgen-interface 時,必須先期編譯 Clojure 來源才能產生 java 類別。

這可以透過呼叫 compile 來完成。編譯類別檔案的預設目的地是 classes/,需要建立並新增至 classpath

$ mkdir classes

編輯 deps.edn 以將 "classes" 新增至路徑

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

src/my_class.clj 中使用 gen-class 宣告類別

(ns my-class)

(gen-class
  :name my_class.MyClass
  :methods [[hello [] String]])

(defn -hello [this]
  "Hello, World!")

然後,您可以在另一個原始檔 src/hello.clj 中使用 :import 參照類別。請注意,命名空間也已新增至 :require 中,因此編譯可以自動找出所有依賴的命名空間並編譯它們。

(ns hello
  (:require [my-class])
  (:import (my_class MyClass)))

(defn -main [& args]
  (let [inst (MyClass.)]
    (println (.hello inst))))

您可以在 REPL 中編譯或執行指令碼來進行編譯

$ clj -M -e "(compile 'hello)"

然後執行 hello 命名空間

$ clj -M -m hello
Hello, World!

請參閱 編譯與類別產生 以取得完整參考。

執行 socket 伺服器遠端 REPL

Clojure 提供內建支援來執行 socket 伺服器,特別是使用它們來主機遠端 REPL。

若要設定 socket 伺服器 REPL,請將下列基本設定新增至您的 deps.edn

{:aliases
 {:repl-server
  {:exec-fn clojure.core.server/start-server
   :exec-args {:name "repl-server"
               :port 5555
               :accept clojure.core.server/repl
               :server-daemon false}}}}

然後透過呼叫別名來啟動伺服器

clojure -X:repl-server

如果您願意,您也可以在命令列中覆寫預設參數(或新增其他選項)

clojure -X:repl-server :port 51234

您可以使用 netcat 從另一個終端機連線

nc localhost 51234
user=> (+ 1 1)
2

使用 Ctrl-D 退出 REPL,使用 Ctrl-C 退出伺服器。

列出所有依賴項

內建 :deps 別名中有多個有用的工具,可探索專案使用的所有傳遞式依賴項(及其授權)。

若要 列出類別路徑中包含的所有依賴項,請使用 clj -X:deps list。例如,在本指南開頭的 hello-world 應用程式中,您會看到類似以下內容

% clj -X:deps list
clojure.java-time/clojure.java-time 1.1.0  (MIT)
org.clojure/clojure 1.11.2  (EPL-1.0)
org.clojure/core.specs.alpha 0.2.62  (EPL-1.0)
org.clojure/spec.alpha 0.3.218  (EPL-1.0)
time-lib/time-lib ../cli-getting-started/time-lib

應用程式使用的所有傳遞式依賴項會依照字母順序列出,並附上版本和授權。請參閱 API 文件以取得其他列印選項。

如果您想要了解依賴項的 樹狀 結構以及如何進行版本選取,請使用 clj -X:deps tree

% clj -X:deps tree
org.clojure/clojure 1.11.2
  . org.clojure/spec.alpha 0.3.218
  . org.clojure/core.specs.alpha 0.2.62
time-lib/time-lib /Users/alex.miller/tmp/cli-getting-started/time-lib
  . clojure.java-time/clojure.java-time 1.1.0

這裡沒有進行任何版本選取,但請參閱 文件 以進一步了解在需要時如何在樹狀結構中說明選取。

這兩個輔助函數都接受一個選用的 :aliases 引數,如果您希望使用一個或多個別名來檢查依賴項清單或樹狀結構,例如 clj -X:deps list :aliases '[:alias1 :alias2]'

原始作者:Alex Miller