Clojure 的公理:什么是语言构建不出来的

2026/4/6
ClojureClojureScript语言设计Lisp函数式编程

问题的本质

Scheme 的哲学是极简主义:5 个特殊形式(defineiflambdaset!begin)+ quote,其余一切自举构建。SICP 用这一套东西从零开始讲完了整个计算机科学。

Clojure 不是 Scheme。它更务实,特殊形式多一些,但依然有一个清晰的分层:有些东西是语言必须提供的,剩下的全都可以用这些基元构建出来

这篇回答一个问题:Clojure 的不可约基元是什么?


第一层:读取器(Reader)—— 代码即数据的根基

Clojure 程序是一段文本。读取器把文本变成数据结构。这一层比”求值”更底层。

;; 你写的文本
(+ 1 2)

;; 读取器产出的数据结构
;; 一个 list,包含三个元素:symbol + 和 number 1、2

读取器认识的数据结构字面量:

字面量数据类型说明
42number整数/浮点
"hello"string字符串
:fookeyword关键字
true / false / nilboolean / nil布尔和空值
()list链表(代码的基本形态)
[]vector向量
{}map映射
#{}set集合

还有读取器宏(reader macros)——在读取阶段就展开的语法糖:

'x       →  (quote x)           ;; 阻止求值
#'x      →  (var x)             ;; 取 var 本身
@x       →  (deref x)           ;; 解引用
#(...)   →  (fn [p1__auto__] …) ;; 匿名函数
#"..."   →  正则对象

这一层不可约:读取器是编译器的一部分,不是 Clojure 代码实现的。它定义了”代码长什么样”。

关键洞察:读取器产出的都是数据结构,不是 AST 节点。这就是 homoiconicity(代码即数据)的根基——(+ 1 2) 既是一段代码,也是一个包含三个元素的 list。没有中间表示。


第二层:求值器(Evaluator)—— 一个规则 + 特殊形式

读取器产出数据后,求值器对它求值。求值器的核心规则只有一条:

如果是一个 list,把第一个元素当函数/特殊形式,其余当参数。

但有些东西这个规则处理不了——比如 if 不应该同时求值两个分支,quote 应该阻止求值,def 需要操作命名空间注册表。这些东西叫特殊形式(special forms)

Clojure 的全部特殊形式:

;; 1. def —— 全局绑定(在命名空间注册)
(def x 42)

;; 2. if —— 条件分支(只求值被选中的分支)
(if test then else)

;; 3. do —— 顺序执行多个表达式,返回最后一个
(do (println "a") (println "b") 42)

;; 4. let —— 局部绑定
(let [x 1 y 2] (+ x y))

;; 5. fn —— lambda(创建闭包)
(fn [x] (* x x))

;; 6. loop / recur —— 尾递归循环
(loop [i 0] (if (< i 10) (recur (inc i)) i))

;; 7. quote —— 阻止求值,返回数据本身
(quote (+ 1 2))  ;; => (+ 1 2)

;; 8. var —— 获取 Var 对象本身(而非其值)
(var x)  ;; => #'user/x

;; 9. throw / try —— 异常
(try (throw (ex-info "err" {})) (catch :default e e))

;; 10. . —— 宿主互操作(方法调用/字段访问)
(.length "hello")  ;; => 5

;; 11. new —— 创建宿主对象
(new js/Error "msg")

;; 12. set! —— 赋值(仅用于宿主互操作和 volatile)
(set! (.-title js/document) "new")

12 个特殊形式(ClojureScript 中 monitor-enter/monitor-exit 不使用)。这就是 Clojure 语言的全部公理。其余一切都是用这些基元构建的。

为什么这些不可约?

if 不可约:因为函数调用会先求值所有参数。如果你用函数模拟 if

;; 假设的 my-if 函数(行不通!)
(defn my-if [test then else]
  (cond test then :else else))

;; 问题:then 和 else 在传入函数之前就被求值了
(my-if true (println "yes") (println "no"))
;; => 打印 "yes" 和 "no" 两行!

if 必须是特殊形式,因为它需要只求值被选中的那个分支

fn 不可约:因为函数需要捕获词法作用域(闭包),这个行为必须由运行时支持。

let 不可约:因为局部绑定需要创建新的作用域,且不先求值绑定符号。

def 不可约:因为需要在命名空间注册表中创建新的映射。

quote 不可约:因为需要阻止求值,而任何函数调用都会先求值参数。

. 不可约:因为需要直接调用宿主平台的方法,绕过 Clojure 的函数调用机制。


第三层:一切可构建的—— 用公理构建的免费赠品

宏是免费的

宏是编译时运行的函数——接收代码(数据),输出代码(数据)。展开后的代码只包含特殊形式和函数调用。

;; defn 是宏,展开成 def + fn
(defn add [a b] (+ a b))
;; 展开 ↓
(def add (fn [a b] (+ a b)))

;; when 是宏,展开成 if + do
(when test body1 body2)
;; 展开 ↓
(if test (do body1 body2))

;; cond 是宏,展开成嵌套的 if
(cond test1 val1 test2 val2 :else val3)
;; 展开 ↓
(if test1 val1 (if test2 val2 val3))

;; and 是宏,展开成嵌套的 if
(and a b c)
;; 展开 ↓
(if a (if b c nil) nil)

;; -> 线程宏
(-> x (f 1) (g 2))
;; 展开 ↓
(g (f x 1) 2)

;; ->> 线程宏
(->> x (map f) (filter g))
;; 展开 ↓
(filter g (map f x))

所有宏都不增加语言的能力——它们只消除重复、改善表达。任何宏展开后的代码,都只包含特殊形式和函数调用。

函数也是免费的

defnmapfilterreduceconjassoc……这些都是函数,用 fndef 定义的普通函数。它们不增加语言的能力——只是标准库。

Protocol 是免费的

(defprotocol Drawable (draw [this]))

展开后是 def + 一些函数 + 一些元数据操作。没有新的特殊形式。

defrecord 是免费的

(defrecord Circle [radius] Drawable (draw [this] ...))

展开后是 def + deftype + 一些函数。没有新的特殊形式。

多重分派是免费的

(defmulti handle :type)
(defmethod handle :click [e] ...)

展开后是 def + 一个 atom(存储分发表)+ 一些函数。没有新的特殊形式。

命名空间是免费的

(ns my-app.core (:require [foo :as f]))

展开后是对编译器命名空间注册表的操作。运行时就是几个 JS 对象。


对比:各语言的最小基元

语言特殊形式数量基本思路
Scheme~5Lambda 演算,极简主义
Clojure~12Lambda + 宿主平台 + 实用主义
JS很多(无法列出)语法复杂,关键字几十个
Python非常多语法驱动,不是数据驱动

Scheme 的 5 个特殊形式:defineiflambdaset!begin(加上 quote)。

Clojure 比 Scheme 多了什么?

  • let(Scheme 用 lambda 模拟,Clojure 为了性能直接提供)
  • loop/recur(Scheme 依赖正确的尾调用,JVM/JS 不保证 TCO,所以 Clojure 显式提供)
  • def(Scheme 的 define 简化版,Clojure 需要和命名空间交互)
  • try/throw(Scheme 没有内建异常)
  • ./new/set!(宿主互操作,纯 Scheme 不需要)

Clojure 比 Scheme 多出的特殊形式,几乎都来自两个选择:1) 宿主平台互操作(JVM/JS),2) 在宿主平台不支持 TCO 时显式处理尾递归。

这些不是设计上的妥协,而是务实的选择——用多几个特殊形式换来在 JVM 和 JS 上的高效运行。


最终的公理体系

不可约的(语言提供):
  ├── 读取器(Reader)       文本 → 数据结构
  │   └── 字面量语法          (), [], {}, #{}, :, ', #', @, ...
  ├── 求值器(Evaluator)     数据结构 → 值
  │   └── 一条规则             list 的第一个元素当函数,其余当参数
  └── 特殊形式(12 个)       求值器无法用自身规则处理的东西
      ├── def                 全局绑定
      ├── if                  条件分支
      ├── do                  顺序执行
      ├── let                 局部绑定
      ├── fn                  lambda
      ├── loop / recur        尾递归
      ├── quote               阻止求值
      ├── var                 Var 引用
      ├── throw / try         异常
      └── . / new / set!      宿主互操作

可构建的(用上面的公理构建):
  ├── 宏(defn, when, cond, and, or, ->, ->>, ...)
  ├── 函数(map, filter, reduce, conj, assoc, ...)
  ├── 数据结构实现(persistent vector/hamt/...)
  ├── defmulti / defmethod    用 atom + 函数
  ├── defprotocol             用函数 + 元数据
  ├── defrecord / deftype     用 def + fn + .
  ├── 命名空间                 用 def + 编译器注册表
  └── 整个标准库

对自己的意义

理解了这个分层之后,你在用 Clojure 时的心态会不一样:

  1. 遇到新语法,先问”这是宏还是特殊形式?“——99% 的情况下是宏。展开它就懂了。
  2. 遇到复杂抽象,知道它最终归约到哪里——都是 fn + def + if
  3. 想发明新抽象,宏给你这个能力——你可以在不增加特殊形式的情况下扩展语言。

这也是 Lisp 家族最独特的力量:语言的基元极少且清晰,其余一切都可以被用户重定义、替换、或增强。这不是”灵活性”,这是”你站在和语言设计者同一个层面上”。


参考资源

📝 文章反馈