Clojure

依赖和 CLI 指南

Clojure 提供用于以下操作的命令行工具:

  • 运行交互式 REPL(读取-求值-打印循环)

  • 运行 Clojure 程序

  • 求值 Clojure 表达式

在所有上述场景中,您可能希望使用其他 Clojure 和 Java 库(依赖项或“deps”)。这些可能是您在本地编写的库,git 中的项目(例如,在 GitHub 上),或者更常见的是,在 Maven 生态系统中可用并由 Maven Central 或 Clojars 等中央存储库托管的库。

在所有情况下,使用库都涉及:

  1. 指定要使用的库,提供其名称和其他方面,如版本

  2. 从 git 或 maven 存储库获取它(一次)到您的本地机器

  3. 使其在 JVM 类路径上可用,以便 Clojure 在您的 REPL 或程序运行时可以找到它

Clojure 工具指定了一种语法和文件 (deps.edn) 用于 (a),在这种情况下,它们将自动处理 (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,可以通过 clj 使用 -X 执行

$ 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 依赖项。您需要收集以下信息

  • 存储库 lib - Clojure CLI 使用一种约定,如果您使用类似 io.github.yourname/time-lib 的库名称,则无需指定 URL,对应 GitHub url 为 https://github.com/yourname/time-lib.git

  • tag - v0.0.1 是我们在上面创建的

  • sha - 标签处的短 sha,如果您本地拥有该 repo,则使用 git rev-parse --short v0.0.1^{commit} 查找它,或者如果您是远程的,则使用 git ls-remote https://github.com/yourname/time-lib.git v0.0.1 查找它。您也可以使用 GitHub repo 查看标签及其支持的提交。

更新 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 别名添加到修改依赖项解析中来将此依赖项添加到您的类路径中: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

请注意,由于转义规则,最好将配置数据放在单引号中。

准备源代码依赖库

一些依赖项在可以在类路径上使用之前需要一个准备步骤。这些库应该在其 deps.edn 中说明这种需求

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

包含顶层键 :deps/prep-lib 告诉 tools.deps 类路径构建,需要额外的东西来准备此库,并且可以通过调用 :build 别名中的 compile 函数来执行。一旦准备步骤完成,它应该创建路径 "target/classes",并且可以检查是否已完成。

您像使用任何其他基于源代码的库一样依赖此库(可能是 git 或本地)

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

如果您随后尝试将该库包含到您的类路径中,您将看到一个错误

$ 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/,需要创建并将它添加到类路径中

$ 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!

有关完整的参考,请参见 编译和类生成

运行套接字服务器远程 REPL

Clojure 提供了对运行 套接字服务器 的内置支持,特别是使用它们来托管远程 REPL。

要配置套接字服务器 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