summaryrefslogtreecommitdiff
path: root/content/article/tutorial-clj-0.md
diff options
context:
space:
mode:
authoreltrac <hi@eltr.ac>2026-09-26 10:06:26 +0800
committereltrac <hi@eltr.ac>2026-09-26 10:06:26 +0800
commit1dbd4e3c0d7e76e491e6e665e3ff73386f9450ec (patch)
tree2a95f697049dd70fe2663728b14d9868974a417a /content/article/tutorial-clj-0.md
parent1e374581fad4a7f967526aade498932508f13657 (diff)
[draft] rename clojure tutorials
Diffstat (limited to 'content/article/tutorial-clj-0.md')
-rw-r--r--content/article/tutorial-clj-0.md259
1 files changed, 0 insertions, 259 deletions
diff --git a/content/article/tutorial-clj-0.md b/content/article/tutorial-clj-0.md
deleted file mode 100644
index 444ba2aa..00000000
--- a/content/article/tutorial-clj-0.md
+++ /dev/null
@@ -1,259 +0,0 @@
-+++
-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》。
-
-希望我不会事无巨细、毫无保留地把某个概念讲得十分彻底,最后写成令人望而生畏的长文。我也会更多关注「为什么」而非只是「是什么」,尽力让读者理解某个东西为什么要存在于这门语言里,为什么要这么写。此外,这篇教程是写给有编程基础的人读的,读者至少要熟悉一门编程语言才能继续。
-
----
-
-## Show Me The Code!
-
-我在了解一门编程语言之前,总想要先有些视觉印象,所以会找些代码片段来看。我说的不是 Hello World,而是真实的代码。
-
-下面是一段从我的项目里摘取出来的[代码片段](https://codeberg.org/eltrac/weepinbell/src/commit/2ddf4e3fe155b1e4ff5dde8a587758d37d665ed4/src/weepinbell/webhook.clj#L16-L30)。
-
-```clojure
-(defn trigger
- "Send requests to a list of webhooks.
- Given a vector/list of URLs, it sends plain POST request to each URL.
- Given a keyword and a map of URL vectors/list, it sends request to each URL in (:keyword map)."
- ([urls]
- (trigger urls nil))
- ([keyw-or-urls webhooks]
- (when-let [hooks (if (nil? webhooks)
- keyw-or-urls
- (->> (get webhooks keyw-or-urls)
- (filter string?)
- (filter valid-http-url?)))]
- (when (keyword? keyw-or-urls)
- (timbre/info "Webhook" (name keyw-or-urls) "triggered."))
- (->> hooks (map send-webhook) doall))))
-```
-
-如果你第一次接触 Lisp,那么你可能注意到这段代码不仅有奇怪的缩进,还有很多、很多的*括号*。
-
-不过,初学者也能发现一些熟悉的东西,比如字符串 `"Webhook"` `"triggeerd."`,还有条件语句 `if`。看样子,`defn` 和 Python 的 `def`、Go 的 `func` 一样,用来定义函数。
-
-其他的也不难理解,比如第 10 行的 `(get webhooks keyw-or-urls)`,如果换个写法,读者可能就能看懂一些了:
-
-```
-get(webhooks, keyw-or-urls)
-```
-
-这段代码中的 `webhooks` 是映射(map),里面存的是键值对;`keyw-or-urls` 在这里是 `keyw(ord)`(关键词),也就是键值对的「键」;`get` 函数从 `webhooks` 中取出指定键的值。(之所以有 `-or-urls`,是因为这是门动态类型语言,这个值也可能本身就是我们要的 URL,不必从 `webhooks` 数组中取出)
-
-没看懂也没关系,上面的代码不重要。
-
-Clojure(以及所有的 Lisp)中的函数调用都写作 `(函数名 参数1 参数2 ...)` 的形式,符号之间一般用空格分隔。
-
-这看起来可能不像函数调用,倒像个数组,或者说列表(list)。不仅是函数调用,函数定义、宏、条件语句、循环结构,以及包括列表本身在内的数据,都是列表。Lisp 的意思是 *LISt Processing*(列表处理)。
-
-函数定义是这么写的:
-
-```clojure
-(defn function-name [arg1 arg2]
- (println "I'm a function!")
- (str arg1 arg2))
-```
-
-这也是个列表,列表的元素分别是 `defn`、`function-name`、`[arg1 arg2]` 以及后面的函数体。
-
-如果要表示列表数据呢?可以这么写:
-
-```clojure
-(list "a" "b" "c")
-;; => ("a" "b" "c")
-```
-
-函数调用和函数定义的写法一样,代码和数据的写法一样,除了个别语法糖,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)和列表(list)。括号里不能再分的东西就是原子,比如 `list` 和 `"a"`,而 `(list "a" "b" "c")` 是列表。
-
-对原子执行 `eval` 操作,或者说求值(evaluate),得到的是原子本身,比如 `(eval 1)` 得到的就是 `1`(因此原子更专业的名字是 *[Self-evaluating objects](https://www.lispworks.com/documentation/HyperSpec/Body/26_glo_s.htm#self-evaluating_object)*,自求值对象);对列表求值,意思就是把列表当作形式(form)而非数据对象(object),形式的第一个元素是操作符(operator),剩下的是参数(arguments)——这就是我们在前一节建立的对 Clojure 的理解。
-
-1. 原子(atom):不是列表、不可再分的数据对象(object)。
-2. 列表(list):包裹在括号内的一个或多个原子。[^2]
-3. 对象(object):不被求值的原子或列表。
-4. 形式(form):被求值的列表,第一个元素是操作符,剩下的元素是参数(被求值的原子是自求值对象,求值的结果是它本身)
-
-其实我们一直预设我们写的所有 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')
-```
-
-于是 `("a" "b" "c")` 这个列表被当作形式,`"a"` 被当作操作符,而 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 写的,但代码很易读。
-
-[^2]: 之后会讲到,列表实际上是用 cons 对构造的。