summaryrefslogtreecommitdiff
path: root/content
diff options
context:
space:
mode:
authoreltrac <hi@eltr.ac>2026-10-03 00:29:31 +0800
committereltrac <hi@eltr.ac>2026-10-03 00:29:31 +0800
commit62f8a94bc166e9899eb88c8c1d2398ddf16c2711 (patch)
treed1fc0e917ff5e1246397d096c32516b8643b2007 /content
parent58dc14a71b3f66bc24acfee0948a88e79643772e (diff)
[draft] update
Diffstat (limited to 'content')
-rw-r--r--content/article/clj5.md180
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 的包名都又臭又长。~~