diff options
Diffstat (limited to 'content/article')
| -rw-r--r-- | content/article/clj5.md | 180 |
1 files changed, 178 insertions, 2 deletions
diff --git a/content/article/clj5.md b/content/article/clj5.md index bdaff300..a426694b 100644 --- a/content/article/clj5.md +++ b/content/article/clj5.md @@ -3,7 +3,7 @@ title = 'Coffee Break Clojure, Vol.5' subtitle = '管理 Clojure 项目' slug = '' tags = [] -categories = '' +categories = '茶歇蔻芥' date = 2026-09-30T15:06:43+08:00 draft = true toc = true @@ -89,7 +89,15 @@ cd antfighter 更多的选项会在后文提及,也可以查看官方提供的 [sample.project.clj](https://codeberg.org/leiningen/leiningen/src/branch/stable/sample.project.clj)。 -## 核心 +## 命名空间与符号 + +之前我们简单略过了命名空间(namespace)的概念,可以理解为符号(symbol)的集合,一个命名空间下的符号不能有重合,命名空间可以相互引用对方的符号。 + +在 Clojure 里,用 `ns` 进入一个命名空间[^1]。如果你在 REPL 里想要直接访问某个命名空间里的符号,可以输入 `(ns project.namespace)`;在文件开头写 `ns`,就可以在这个命名空间里定义符号。 + +`ns` 的第一个参数是命名空间的名字,与当前文档在 `src/` 的子目录对应。比如,如果文件被放在 `src/antfighter/core.clj`,这个文件的命名空间只能叫 `antfighter.core`。子目录同理,`src/antfighter/http/client.clj` 是 `antfighter.http.client`。Clojure 文件不应该包含 `-`,而是用 `_`,但是在命名空间的命名里要变成 `-`,比如 `src/antfighter/meat_proxy.clj` 应该是 `antfighter.meat-proxy`。 + +以下是我们刚才生成的示例代码中的 `core.clj` 文件: ```clojure (ns antfighter.core @@ -101,6 +109,19 @@ cd antfighter (println "Hello, World!")) ``` +这个文件的 `ns` 还包含更多的参数,写在 `ns` 内部的 `:gen-class` 等价于直接写 `gen-class`。同理还有 `:require` 和 `:import`,之后会谈到。 + +```clojure +(ns antfighter.core) +(gen-class) +``` + +`gen-class` 的作用是生成 Java 类(Generate Class)。如果你需要把 Clojure 程序编译成 JVM 字节码(`.jar` 文件),然后直接在 Java 环境里执行,就需要给**主**命名空间生成 Java 类。其他命名空间是通过 Clojure 调用的,只有 `core.clj` 才需要 Java 类。 + +如果直接用 `clj` 或 `lein` 命令行,就自带 Clojure 运行环境,不需要 `gen-class`。 + +继续,看刚才一同生成的 `core_test.clj`,可以注意到 `test/` 目录下的文件也对应一个命名空间,`test/antfighter/core_test.clj` 对应 `antfighter.core-test`。稍后再来看测试怎么写。 + ```clojure (ns antfighter.core-test (:require [clojure.test :refer :all] @@ -111,9 +132,164 @@ cd antfighter (is (= 0 1)))) ``` +此外还有 `:require`,用于引入另一个命名空间的符号。`require` 接收可变数量的参数,每个参数都是一个向量:第一个元素是命名空间名,第二元素是 `:refer` 或 `:alias`,第三个元素根据第二个元素而定。向量也可以只有一个元素。 + +- `[project.namespace]` + - 可以在当前命名空间使用 `project.namespace` 中的符号。假设 `project.namespace` 中有 `say` 符号,可以用 `project.namespace/say` 引用。 +- `[project.namespace :alias pn]` + - 可以在当前命名空间用 `pn` 作为 `project.namespace` 的别名。 + - 例如,`project.namespace/say` 可以写作 `pn/say`。 +- `[project.namespace :refer [symbol]]` + - 可以在当前命名空间中直接使用 `project.namespace` 中的 `symbol`,不需要写 `/` 以及命名空间的名字。 + - 可以在 `:refer` 之后的向量中填写多个符号名。 + - 可以把 `:refer` 之后的向量替换成 `:all`,把命名空间中的所有符号引入当前命名空间。不推荐这种写法,需要注意命名冲突。 + +其实还有 `:use` 的写法,等同于 `:require [... :refer all]`。`:use` 也可以加上 `:exclude`(排除)和 `:only`(只引入)等参数,但根据 [Clojure Style Guide](https://guide.clojure.style/) 的[讨论](https://github.com/bbatsov/clojure-style-guide/issues/5),现在已经不推荐使用 `:use`,全部换成 `:require` 是最好的。 + +至于 `:import`,这是用来引入 Java 库的,这个我们留到之后讲 Clojure 如何与 Java 联动的时候再说。 + ## 引入依赖 +Clojure 有多种声明依赖的方式,被原生支持的是 `deps.edn`,写法如下: + +```clojure +{:deps {org.clojure/clojure {:mvn/version "1.10.3"}} + ;; ... + } +``` + +不过我们在用 Leiningen,所以可以直接在 `project.clj` 里声明: + +```clojure +(defproject antfighter "0.1.0-SNAPSHOT" + :dependencies [[org.clojure/clojure "1.12.2"]] + ;; ... + ) +``` + +Leiningen 的写法干净不少。Leiningen 和 `deps.edn` 都默认用 [Maven Central](https://repo1.maven.org/maven2/) 和 [Clojars](https://clojars.org/) 作为依赖的来源。Clojars 是个专门用来发布和存放 Clojure 依赖的 [Maven 仓库](https://mvnrepository.com/repos/clojars),与 Maven 相关的工具兼容。不过我们用的是 Leiningen,不需要考虑太多细节。 + +假设我们要解析 JSON,需要引入 [Cheshire](https://github.com/dakrone/cheshire) 库[^2]。可以先去这个仓库的 `README` 里找到依赖字符串 `[cheshire "6.2.0"]`,然后复制到 `project.clj` 里。也可以直接在 Clojars 上直接搜索依赖。 + +```clojure +(defproject antfighter "0.1.0-SNAPSHOT" + :dependencies [[org.clojure/clojure "1.12.2"] + [cheshire "6.2.0"]] + ;; ... + ) +``` + +保存之后执行 `lein deps` 可以下载依赖。用 `lein repl` 启动 REPL,或者用 `lein run` 运行程序的时候也会自动下载依赖。 + +写个文件测试一下,把 Clojure 数据转换为 JSON 字符串,再把字符串转换为 Clojure 数据试试。 + +```clojure +(ns antfighter.cat + (:require [cheshire.core :refer [generate-string parse-string]])) + +(generate-string {:foo "bar" :key "val"}) +;; => "{\"foo\":\"bar\",\"key\":\"val\"}" + +(parse-string "{\"foo\":\"bar\",\"key\":\"val\"}") +;; => {"foo" "bar", "key" "val"} +``` + +好了,现在你已经会导包了,可以说已经跨进 Clojure 生态的大门了。 + +## 偷渡 Java 代码 + +Clojure 毕竟是门 Lisp,用的人相对较少,所以有时候会找不到想要的包。这时就可以从生态更成熟的 Java 那边偷来用。 + +还记得 Leiningen 也从 Maven Central 拉取依赖吗?Java 的各种 `.jar` 就放在 Maven Central 上,所以可以直接拿来用。 + +比方说,我想用 [Apache Commons Validator](https://mvnrepository.com/artifact/commons-validator/commons-validator)(用于检验数据有效性的库,比如 URL 是否合法),将依赖名和版本号写进 `project.clj` 即可。格式上,Java 的依赖和 Clojure 的依赖没有什么区别,它们实际上也都只是 `.jar` 文件而已。 + +```clojure +(defproject antfighter "0.1.0-SNAPSHOT" + :dependencies [[org.clojure/clojure "1.12.2"] + [cheshire "6.2.0"] + [commons-validator "1.11.0"]] + ;; ... + ) +``` + +Clojure 的 `import` 跟 Java 的差不了太多,`import` 的参数是 Java 的包名加上类名。比方说,刚才的 Apache Commons Validator 可以这么用: + +```clojure +(ns antfighter.spider + (:import [org.apache.commons.validator.routines UrlValidator])) + +(defn valid-http-url? [url] + (let [schemes (into-array String ["http" "https"]) + validator (UrlValidator. schemes)] + (.isValid validator url))) +``` + +`import` 和 `require` 类似,也可以接收多个向量。每个向量的第一个元素是包名,后面的是可变数量的元素,是 Java 类名。上述代码从 `org.apache.commons.validator.routines` 这个包[^3]里引用了 `UrlValidator` 类。 + +顺带一提,Clojure 做的一大改变就是把类和命名空间解耦了。在 Java 中,类既是数据类型,又是命名空间。在 Clojure 创造者 Rich Hickey 的演讲《[Simple Made Easy](https://redirect.invidious.io/watch?v=SxdOUGdseq4)》就提到了这一点。命名空间就只是命名空间而已。 + +除了在 `project.clj` 里声明的外部 Java 库,也可以直接用 Java 标准库里的类。 + +```clojure +(import [java.net URI URL]) +;; 上述代码引入了 java.net 的包 +;; 里的 URI 和 URL 类 +``` + +你可能注意到了,我们引入的是 Java 类,不是 Clojure 符号,处理起来会有点困难。Clojure 提供了点语法。`.` 是 Clojure 的特殊形式,和 Java 的 `.` 类似。 + +```java +import java.util.Date + +Date date = new Date() +date.getMonth() +``` + +上述 Java 代码相当于: + +```clojure +(import [java.util Date]) + +(def date (new Date)) +(. date getMonth) +``` + +上述 Clojure 代码还可以进一步简写: + +```clojure +(import [java.util Date]) + +(def date (Date.)) +(.getMonth (Date.)) +``` + +`new Object` 相当于 `Object.`,这是 Clojure 提供的语法糖。如果要用点语法调用对象的方法,可以在方法名前面加上 `.`,当成函数来用,函数的第一个参数就是对象本身,像这样:`(.method object args...)`。 + +还可以进一步简写: + +```clojure +(.getMonth (java.util.Date.)) +;; => 10 +``` + +如果你熟悉 Java,你应该知道除了必须把类实例化为对象才能调用的方法,还有静态方法。静态方法可以直接在类上调用,不需要创建对象。对于静态方法,只需要把 Java 类当成 Clojure 命名空间,把方法当作命名空间里的函数,也就是用 `/` 语法。 + +```clojure +(java.time.Instant/now) +;; => #object[java.time.Instant 0x1c2b8e81 "2026-10-02T16:26:17.401045Z"] +``` + +好了,现在你已经会偷 Java 代码用了。 + ## Leiningen 插件 ## 只给测试用的依赖 +## Footnotes + +[^1]: `ns` 实际上只是把全局变量 `*ns*` 设置成了指定的命名空间。 + +[^2]: Cheshire 指《爱丽丝梦游仙境》里的「柴郡猫」。`cheshire` 库用的是 Java 库 Jackson。 + +[^3]: ~~是的,Java 的包名都又臭又长。~~ |
