diff options
Diffstat (limited to 'content/article/tutorial-clj-0.md')
| -rw-r--r-- | content/article/tutorial-clj-0.md | 259 |
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 对构造的。 |
