summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--content/article/tutorial-clj-0.md217
1 files changed, 192 insertions, 25 deletions
diff --git a/content/article/tutorial-clj-0.md b/content/article/tutorial-clj-0.md
index 7b2a5952..af5c02c1 100644
--- a/content/article/tutorial-clj-0.md
+++ b/content/article/tutorial-clj-0.md
@@ -1,35 +1,32 @@
+++
-title = 'Coffee Break Clojure #0'
-subtitle = '什么是 Clojure?'
-slug = ''
-tags = []
-categories = ''
-date = 2026-09-15T12:50:00+08:00
-draft = true
+title = 'Coffee Break Clojure, Vol.0'
+subtitle = '初识 Clojure 与 Lisp'
+slug = 'tutorial-clj-0'
+tags = ["Lisp","编程"]
+categories = '茶歇蔻芥'
+date = 2026-09-15 22:58:43
+draft = false
toc = true
+endnotes = [
+ "我只是受《A Scheme Primer》的语言风格启发,并不是妄想写一篇同等量级和地位的文本。",
+ "若有**事实**问题和**技术性错误**,还请指出。"
+]
+fedi = 'https://akk.eltr.ac/objects/e5a14eb5-f7e6-41f3-834a-d3b741fa857f'
+++
如果有人问我最喜欢的编程语言是什么,我可能会在 Clojure 和 Go 之间纠结一番,但无疑我更*享受*手写 Clojure 的体验。可就像大部分 Lisp 一样,Clojure 的讨论度并不高,中文内容更是少得可怜。所以我为什么不能贡献一些内容呢?
-我前段时间听过一档名为《Coffee Break French》的播客,内容是可以在午休时喝着咖啡听完的法语对话。学习一门编程语言怎么不可以这样轻松呢?我最近也有在帮忙翻译《[A Scheme Primer](https://files.spritely.institute/papers/scheme-primer.html)》,这篇教程的语言也相当*平易近人*。受其启发,再加上我一直想要练习节制地写作,而不是滔滔不绝地写上好几千字,所以我决定开启新的博客系列:《Coffee Break Clojure》。
+我前段时间听过一档名为《Coffee Break French》的播客,内容是可以在午休时喝着咖啡听完的法语对话。学习一门编程语言怎么不可以这样轻松呢?我最近也有在帮忙翻译《[A Scheme Primer](https://files.spritely.institute/papers/scheme-primer.html)》,这篇教程的语言相当通俗。受其启发,再加上我一直想要练习节制地写作,而不是滔滔不绝地写上好几千字,所以我决定开启新的博客系列:《Coffee Break Clojure》。
-我希望我不会事无巨细、毫无保留地把某个概念讲得十分彻底,最后写成令人望而生畏的长文。我也会更多关注「为什么」而只是「是什么」,尽力让读者理解某个东西为什么要存在于这门语言里,为什么要这么写。
-
----
-
-我只是受《A Scheme Primer》的语言风格启发,并不是妄想写一篇同等量级和地位的文本。我对 Clojure 的了解也没有特别深入,我甚至没有自信讲清楚十分重要的宏(macro)。不过,至少我有一些实操经验,也踩过坑。再者,教是最好的学,也响应 [Goedecke 的号召](https://www.seangoedecke.com/blog-about-things-you-dont-understand-yet/),我觉得我完全有*资格*写这样一篇教程。
-
-若有事实问题和技术性错误,还请指出。
-
-此外,这篇教程是写给有编程基础的人读的,读者至少要熟悉一门编程语言才能继续。
+希望我不会事无巨细、毫无保留地把某个概念讲得十分彻底,最后写成令人望而生畏的长文。我也会更多关注「为什么」而非只是「是什么」,尽力让读者理解某个东西为什么要存在于这门语言里,为什么要这么写。此外,这篇教程是写给有编程基础的人读的,读者至少要熟悉一门编程语言才能继续。
---
-## 初识
+## Show Me The Code!
-什么是 Clojure?我在了解一门编程语言之前,总想要先有些视觉印象,所以会找些代码片段来看。我不要*给小孩子看的* Hello World 程序,我要看真实的代码。不必读懂,感受这门语言的形状就好。
+我在了解一门编程语言之前,总想要先有些视觉印象,所以会找些代码片段来看。我说的不是 Hello World,而是真实的代码。
-下面是一段从我项目里摘取出来的[代码片段](https://codeberg.org/eltrac/weepinbell/src/commit/2ddf4e3fe155b1e4ff5dde8a587758d37d665ed4/src/weepinbell/webhook.clj#L16-L30)。
+下面是一段从我的项目里摘取出来的[代码片段](https://codeberg.org/eltrac/weepinbell/src/commit/2ddf4e3fe155b1e4ff5dde8a587758d37d665ed4/src/weepinbell/webhook.clj#L16-L30)。
```clojure
(defn trigger
@@ -53,15 +50,19 @@ toc = true
不过,初学者也能发现一些熟悉的东西,比如字符串 `"Webhook"` `"triggeerd."`,还有条件语句 `if`。看样子,`defn` 和 Python 的 `def`、Go 的 `func` 一样,用来定义函数。
-其实其他的也不难理解,比如第 10 行的 `(get webhooks keyw-or-urls)`,如果换个写法,读者可能就能看懂一些了:
+其他的也不难理解,比如第 10 行的 `(get webhooks keyw-or-urls)`,如果换个写法,读者可能就能看懂一些了:
```
get(webhooks, keyw-or-urls)
```
-这段代码中的 `webhooks` 是映射(map),里面存的是键值对;`keyw-or-urls` 在这里是 `keyw(ord)`(关键词),也就是键值对的「键」;`get` 函数从 `webhooks` 中取出指定键的值。
+这段代码中的 `webhooks` 是映射(map),里面存的是键值对;`keyw-or-urls` 在这里是 `keyw(ord)`(关键词),也就是键值对的「键」;`get` 函数从 `webhooks` 中取出指定键的值。(之所以有 `-or-urls`,是因为这是门动态类型语言,这个值也可能本身就是我们要的 URL,不必从 `webhooks` 数组中取出)
+
+没看懂也没关系,上面的代码不重要。
+
+Clojure(以及所有的 Lisp)中的函数调用都写作 `(函数名 参数1 参数2 ...)` 的形式,符号之间一般用空格分隔。
-Clojure(以及所有的 Lisp)中的函数调用都写作 `(函数名 参数1 参数2 ...)` 的形式,符号之间用逗号分隔。这看起来可能不像函数调用,倒像个数组,或者说列表(list)。不仅是函数调用,函数定义、宏、条件语句、循环结构,以及包括列表本身在内的数据,都用列表表示。
+这看起来可能不像函数调用,倒像个数组,或者说列表(list)。不仅是函数调用,函数定义、宏、条件语句、循环结构,以及包括列表本身在内的数据,都是列表。Lisp 的意思是 *LISt Processing*(列表处理)。
函数定义是这么写的:
@@ -71,7 +72,7 @@ Clojure(以及所有的 Lisp)中的函数调用都写作 `(函数名 参数1
(str arg1 arg2))
```
-这也是个列表,列表的元素分别是 `defn` `function-name` `[arg1 arg2]` 以及后面的函数体。
+这也是个列表,列表的元素分别是 `defn`、`function-name`、`[arg1 arg2]` 以及后面的函数体。
如果要表示列表数据呢?可以这么写:
@@ -82,5 +83,171 @@ Clojure(以及所有的 Lisp)中的函数调用都写作 `(函数名 参数1
函数调用和函数定义的写法一样,代码和数据的写法一样,除了个别语法糖,Clojure 几乎没有别的语法了,一切都可以用列表表示,Clojure 程序就是一个个相互嵌套的列表。
-## 并不都是函数
+## To eval, or not to eval
+
+回看列表:
+
+```clojure
+(list "a" "b" "c")
+;; => ("a" "b" "c")
+```
+
+它返回的值是 `("a" "b" "c")`。为什么 `"a"` 作为列表的第一个元素,没有被当作操作的名字?为什么 `"b"` 和 `"c"` 没有被当作参数?
+
+带着这个问题,先来理解 Eval 这个概念。你可能在 Python 和 JavaScript 里见过 `eval()` 函数,这个函数把字符串(也就是数据)当作代码来执行,像这样:
+
+```js
+// 你现在就可以按下 Ctrl/Command + Shift + I 打开 Console
+// 在你的浏览器里输入这段 JavaScript 代码:
+
+eval("1 + 1")
+// => 2
+```
+
+JavaScript 也可以用函数构造数组:
+
+```js
+eval('Array("a", "b", "c")')
+// => ['a', 'b', 'c']
+```
+
+上面这段代码和下面这段是等价的:
+
+```js
+Array("a", "b", "c")
+// => ['a', 'b', 'c']
+```
+
+让我们回到 Clojure,下面这两段代码也是等价的:
+
+```clojure
+(list "a" "b" "c")
+;; => ("a" "b" "c")
+(eval '(list "a" "b" "c"))
+;; => ("a" "b" "c")
+```
+
+等等,单独出现的那个 `'` 是什么?为什么不写成字符串?
+
+不妨试试求值字符串会发生什么:
+
+```clojure
+(eval "(list \"a\" \"b\" \"c\")")
+;; => "(list \"a\" \"b\" \"c\")"
+```
+
+得到的是字符串本身!为什么会这样?
+
+在 Clojure(以及所有的 Lisp)里,数据可以大致理解为两类:原子(atom)和形式(form)。括号里不能再分的东西就是原子,比如 `list` 和 `"a"` 都是原子,而 `(list "a" "b" "c")` 是形式。
+
+对原子执行 `eval` 操作,或者说求值(evaluate),得到的是原子本身,比如 `(eval 1)` 得到的就是 `1`;对形式求值,就要把形式的第一个原子当作操作,剩下的原子当作参数——这就是我们在前一节建立的对 Clojure 的理解。
+
+其实我们一直预设我们写的所有 Clojure 代码最终都会被求值。如果把一整个 Clojure 源代码文件当作形式(form)的集合,我们执行源代码的过程其实就是在对其中的形式求值(eval)。
+
+理解 Eval 对理解 Lisp 非常重要,如果没理解,可以多读几遍。
+
+回看这段代码:
+
+```clojure
+(eval "(list \"a\" \"b\" \"c\")")
+;; => "(list \"a\" \"b\" \"c\")"
+```
+
+`eval` 求值的是一个原子,一个字符串值,不是形式。
+
+```clojure
+(eval '(list "a" "b" "c"))
+;; => ("a" "b" "c")
+```
+
+这段代码才是在对形式求值。是时候解释 `'` 是什么了。这个符号其实是个语法糖,上面的代码等价于:
+
+```clojure
+(eval (quote (list "a" "b" "c")))
+;; => ("a" "b" "c")
+```
+
+我们不妨试试,没有这个 `quote`,最终会得到什么结果:
+
+```clojure
+(eval (list "a" "b" "c"))
+;; Execution error (ClassCastException) at user/eval19 (REPL:1).
+;; class java.lang.String cannot be cast to class clojure.lang.IFn (java.lang.String is in module java.base of loader 'bootstrap'; clojure.lang.IFn is in unnamed module of loader 'app')
+```
+
+报错了!捋一下发生了什么:一般来说,执行函数之前,要先对函数的参数求值。`eval` 函数的参数求值后得到 `("a" "b" "c")`,于是上面的代码就变成了:
+
+```clojure
+(eval ("a" "b" "c"))
+;; Execution error (ClassCastException) at user/eval19 (REPL:1).
+;; class java.lang.String cannot be cast to class clojure.lang.IFn (java.lang.String is in module java.base of loader 'bootstrap'; clojure.lang.IFn is in unnamed module of loader 'app')
+```
+
+Clojure 没办法把字符串 `"a"` 转换为函数类型的数据,所以报错了。
+
+要保证 Clojure 不把这个列表误以为是函数调用,就要用到 `quote`。给形式打上引号,让 Clojure 把 `quote` 里面的东西当作数据而不是代码来处理。
+
+写在源代码文件里的形式最终都会在运行时被求值(eval),如果想要一部分形式不被求值、被当作数据处理,就要打引号,用 `quote` 或者 `'` 标记。`quote` 看起来像函数,实际上是个特殊形式(special form),里面的参数不会被求值。
+
+Clojure 代码在运行时默认被求值**一次**,若是多次求值,含义就会不同。
+
+```clojure
+(eval (eval '(list "a" "b" "c")))
+;; Execution error (ClassCastException) at user/eval27 (REPL:1).
+;; class java.lang.String cannot be cast to class clojure.lang.IFn (java.lang.String is in module java.base of loader 'bootstrap'; clojure.lang.IFn is in unnamed module of loader 'app')
+```
+
+不妨练习一下,搞清楚上面这段代码被求值时发生了什么。
+
+还有一些别的小练习:
+
+```clojure
+;; 1. 为什么下面这两段代码是等价的?
+(eval (quote (println "Hello World")))
+(println "Hello World")
+```
+
+```clojure
+;; 2. list 和 quote 的区别是什么?
+(list "a" "b" "c")
+;; => ("a" "b" "c")
+'("a" "b" "c")
+;; => ("a" "b" "c")
+
+(list 1 (+ 1 1) 3)
+;; => (1 2 3)
+'(1 (+ 1 1) 3)
+;; 猜猜这段代码会得到什么结果?
+```
+
+## 代码即数据
+
+不难发现,在 Clojure 中:代码即数据。代码不过是被 Eval 的数据,而数据不过是没有被 Eval 的代码,归根结底都是被括号包起来的形式,区别仅仅在于有没有被 Eval。
+
+这么看来,我们可以对代码做非常有趣的事情:
+
+```clojure
+(cons '+ (list 1 2 3))
+;; => (+ 1 2 3)
+```
+
+如你所见,我们用 `cons` 把 `+` 放在了数字列表的最前面。这个形式被 Eval 了一次得到 `(+ 1 2 3)`,不难想象再 Eval 一次会发生什么。
+
+```clojure
+(eval (cons '+ (list 1 2 3)))
+;; => 6
+```
+
+上述代码有个简便写法[^1]:
+
+```clojure
+(apply + (list 1 2 3))
+;; => 6
+```
+
+往列表里添加元素不过是冰山一角,我们能对数据做什么样的变换,就能对代码做什么样的变换。我们还能借助宏(macro)在求值前变换代码,来构造某些很方便的写法,按自己的想法拓展这门语言。具体的内容会在之后谈到,今天的内容已经够多了。
+
+回见!
+
+[^1]: 虽然大体一致,底层的逻辑其实是不同的,`apply` 并不是 `(eval (cons func args))` 的语法糖。不过,`eval` 和 `apply` 都调用了 `IFn` 接口的 `applyTo` 方法,详见:[clojure.lang.AFn](https://github.com/clojure/clojure/blob/b4ea0f824b2eea039dfc06b796ed601e35cbeab6/src/jvm/clojure/lang/AFn.java#L143-L425)、[clojure.lang.Compiler](https://github.com/clojure/clojure/blob/b4ea0f824b2eea039dfc06b796ed601e35cbeab6/src/jvm/clojure/lang/Compiler.java#L4215-L4231) 和 [clojure.core/apply](https://github.com/clojure/clojure/blob/b4ea0f824b2eea039dfc06b796ed601e35cbeab6/src/clj/clojure/core.clj#L662-L675)。Clojure 虽然是 Java 写的,但代码很易读。