第 17 章

教学者的转译

“在一个中国原则基礎上達成『海峽兩岸同屬一个中国,共同努力谋求国家統一』的九二共识。”

这句话出现在2019年1月2日北京的一场纪念活动上。它重述了一个已有二十七年历史的共识,但重述本身改变了它的表述方式——原本以口头方式各自表述的模糊共识,在重述中被赋予了更明确的措辞。这不是第一次有人试图把九二共识写得更清楚,也不会是最后一次。但每一次重述都在做同一件事:把当年那些参与谈判的人脑子里装着的东西,翻译给从未经历过那个时刻的人听。

翻译从来不是中性的。当一门语言活过十五年,它的遗产便不再只存在于代码仓库中。它开始沉淀在那些教别人使用它的人身上。这些人站在创造者和新来者之间,做着和那场纪念活动上的发言人相似的工作:他们要把某些曾经在特定时刻、特定语境下形成的东西,转译给另一个时代、另一种背景下的听众。他们选择哪些术语保留,哪些替换,哪些简化。每一次选择都在微妙地重塑集体理解。

Clojure社区的教学史,就是这样一部转译史。最早期的Clojure教学几乎完全由创造者和核心团队承担。

2008年到2011年间,如果你想学Clojure,你能找到的材料基本上都出自同一群人之手。Rich Hickey本人的演讲和演示文稿是最权威的来源。Stuart Halloway在2011年出版的《Programming Clojure》是第一本英文教材。还有邮件列表上的讨论——那些讨论本身就是教学现场。一个新人提出疑问,资深成员给出解释,解释中常常引用Hickey在某次演讲中说过的话,或者指向某段源代码中的设计决策。

这个阶段的教学有一个显著特征:教学语言与设计语言高度一致。教程本身就是语言哲学的延伸。当你读Halloway那本书的第一版时,你能感受到他不是在简化什么——他在试图让你理解设计者为什么会做那些选择。不可变数据结构不只是个功能特性,它是Clojure处理时间和状态的整体哲学的一部分。宏不只是个元编程工具,它是Lisp“代码即数据”传统的核心体现。REPL不只是个命令行界面,它是与运行中的程序进行对话的方式。

这些都不是可以轻易压缩成最佳实践清单的东西。它们是一个连贯的思想体系。而早期的教学者之所以能够传递这个体系,是因为他们自己就参与了它的构建过程。他们不需要翻译——他们可以直接讲述,因为那就是他们自己的语言。

但这种状态不可能持续。社区在扩大。2012年之后,Clojure的用户群从最初的函数式编程爱好者和Lisp怀旧者,扩展到了更广泛的人群。这些人中有从Java转过来的企业开发者,有被ClojureScript吸引的前端工程师,有在数据科学领域尝试新工具的分析师。他们中的许多人从未读过邮件列表上的原始讨论。他们没有听过Hickey在2009年做的那个题为《Are We There Yet?》的演讲——在那次演讲中,Hickey用了将近一个小时来阐述他对时间、状态和标识的思考,那是理解Clojure设计哲学的钥匙之一。他们没有参与过关于core.async是否应该进入核心库的漫长辩论。

他们甚至可能不知道“Clojure”这个名字本身就是关于“闭包”的一个双关——既是计算概念,也是社区隐喻。这些人需要不同的入门路径。于是新一代教学者出现了。他们通过博客、会议演讲和在线课程向更广泛的受众解释Clojure。他们不是核心团队成员。他们中的大多数人在Clojure已经相对成熟之后才开始学习这门语言。

他们的权威不来自参与设计决策,而来自他们能够把事情讲清楚的能力。Eric Normand是这一代教学者中的代表人物之一。他在2012年开始写博客,2014年推出了PurelyFunctional.tv网站,提供Clojure和函数式编程的视频课程。Normand的背景是计算机科学教育而非语言设计。他的教学方法不是从哲学原则出发,而是从程序员日常会遇到的问题出发:如何处理集合?如何管理状态?如何组织代码?

他仍然会讲到不可变性,但他更倾向于用避免常见错误的实用理由来解释,而不是从时间模型的本体论开始。这是有效的教学法。

一个想要快速上手的开发者不需要先理解时间进展构造这个概念。他只需要知道,如果你用atom来管理可变状态,你的并发程序就不会出现某些特定类型的错误。这足够让他开始工作了。但这里有一个微妙的代价。当教学从“为什么”转向“怎么做”时,某些东西就丢失了。不是丢失了信息——Normand的课程里包含大量准确的信息——而是丢失了语境。那些设计决策背后的复杂权衡被压缩成了最佳实践。那些曾经在邮件列表上引发过激烈争论的选择,变成了教材中理所当然的陈述。

让我们追踪一个具体概念的教学转译过程:Clojure的序列抽象。在Rich Hickey的2008年演讲中,序列被介绍为一个核心设计决策的结果:Clojure选择让所有集合类型都支持统一的序列操作,而不是为每种集合类型提供单独的函数集。这个设计的理由涉及对Common Lisp序列系统的批评、对Java集合框架的分析、以及对函数式编程中逐个处理元素模式的观察。

Hickey用了大约二十分钟来解释这个设计的来龙去脉。在《Programming Clojure》第一版中,Halloway用了一整章来处理序列。他保留了Hickey分析中的大部分结构,但把它组织得更像教程:先介绍基本操作,再展示组合方式,最后讨论性能考量。哲学讨论被压缩为章节开头的一段概述。到了2015年左右,当新一代教学者在网上写Clojure教程时,序列通常被介绍为Clojure处理集合的方式。一个典型的教程会展示map、filter和reduce等函数如何通用于各种集合类型,然后给出一串代码示例。至于为什么要设计统一的序列抽象——为什么这不是一个显而易见的决定——这个问题很少被提及。

这不是错误。那个教程作者写的东西是正确的。map确实可以用于所有集合类型。但对于一个从未接触过其他Lisp方言的初学者来说,统一的序列抽象听起来就像是一个自然而然的事实,而不是一个需要被论证的设计选择。

共识的基础开始从共同的理解转向共同的习惯。转译的另一个维度发生在术语层面。Clojure社区有一套特定的词汇表。有些词来自Lisp传统:cons、car、cdr——虽然Clojure用first和rest替代了后两个。有些词是Hickey创造的或重新定义的:identity、state、value、reference type。有些词来自函数式编程的一般传统:pure function、side effect、higher-order function。这些词在原始语境中都有精确的含义,而且它们的含义常常与日常直觉不同。状态是Hickey特别关注的一个概念。在Clojure的哲学框架中,状态不是某个时刻变量的值,而是一个随时间推移的身份的一系列值。这是一个反直觉的定义——在日常编程中,我们说程序的状态时通常指的是当前所有变量的值。

但Hickey坚持这种区分,因为这与他对时间建模的整体思路一致:如果你把状态理解为快照序列而不是可变容器,你的并发模型就会完全不同。在早期教学中,这个区分的精确性被小心翼翼地维护着。《Programming Clojure》第一版在介绍引用类型时花了相当篇幅来解释标识与状态的区别。Halloway写道:在Clojure中,标识是一个可以随时间推移而关联不同值的实体;状态是标识在某个特定时刻的值。

这是一个精确但需要读者停下来思考的表述。到了后来的教学材料中,这个区分常常被软化或省略。一本2016年出版的Clojure入门指南在介绍atom时,把它描述为管理可变状态的方式之一,并建议读者可以把它想象成一个安全的变量。这当然更容易理解——新读者已经知道什么是变量——但它恰好抹去了Hickey试图建立的那个关键区分。变量在日常用法中既指标识也指状态,而Clojure的设计恰恰是要把这两者分开。这不是某个教学者偷懒的问题。

这是任何知识传统在向外传播时都会面临的困境。你要么坚持精确性,冒着让新人望而却步的风险;你要么做出简化,接受某些微妙之处会在翻译过程中丢失。

最能体现这种张力的,是一本广受欢迎的Clojure入门书籍的版本演变。《Clojure Programming》由Chas Emerick、Brian Carper和Christophe Grand合著,2012年由O'Reilly出版。这本书在社区中被广泛认为是Clojure教材的标杆之一——它比《Programming Clojure》更面向实践,但保留了足够的深度来让读者理解设计原理。

比较这本书的第一版和后来印刷中的修订版是很有启发的。让我们聚焦于它对宏的处理。在第一版中,宏的介绍从Lisp的代码即数据原则开始讲起。作者们花了大约五页篇幅来解释为什么Lisp的语法结构使得宏成为可能——因为Lisp代码本身被写成Lisp数据结构,所以你可以用操作数据结构的方式来操作代码。

然后他们展示了几个简单的宏示例,最后讨论了宏的适用场景和滥用风险。整个处理大约十五页。在后来的印刷版中,宏的章节被重新组织。代码即数据原则仍然被提及,但它被压缩到了半页的篇幅。新增的内容是一组常见宏模式和帮助读者决定何时使用宏而非函数的决策指南。章节的总体页数没有减少太多——大概从十五页变成了十三页——但重心明显从原理转向了实践。

这个变化反映了读者群的变化。《Clojure Programming》第一版的预期读者是那些已经有一定编程经验、想要深入了解Clojure的人。但随着Clojure用户群的扩大,这本书开始被更多的新手程序员阅读——那些可能从未接触过Lisp的人。对于这些读者来说,代码即数据是一个需要额外解释才能理解的概念,而一组可以直接使用的宏模式则可以直接派上用场。从教学效果来看,这个调整可能是合理的。更多的新手因此能够使用宏来解决实际问题。但从知识传递的角度来看,有某种东西在这个过程中被稀释了。

那些只通过修订版学习宏的人,可能永远不会理解为什么Clojure的宏与其他语言中的元编程机制——比如Python的装饰器或Java的注解——有根本性的不同。他们会使用宏——这是一个好的结果——但他们使用的是一种工具,而不是理解了一种思想。这里出现的不是堕落,而是变形。任何活着的知识传统都必须经历这种变形才能传播。

问题不在于变形本身,而在于变形的累积效应:当足够多的新成员通过转译而非原始文本学习语言时,共识的基础会发生什么变化?2018年发生的一件事可以作为一个观察点。那年夏天,Clojure邮件列表上出现了一个讨论线程,标题是询问为什么map返回的是惰性序列。提问者是一个相对新的社区成员,他注意到Clojure的map函数返回的是惰性序列而不是严格序列,这让他在调试时遇到了一些困惑——错误不会在map调用时立即出现,而是在序列被实际消费时才暴露出来。这个线程很快吸引了大量回复。

一些资深成员解释了惰性求值的设计理由:它允许处理无限序列,避免了不必要的中间集合构造,在某些场景下可以显著提升性能。这些解释都是正确的。但有趣的是这个讨论的走向。在提供了技术解释之后,有人贴出了Rich Hickey在2009年讨论惰性求值设计的一封旧邮件。在那封邮件中,Hickey不仅解释了惰性求值的优势,还讨论了它的代价——调试困难、资源管理复杂、以及在某些情况下可能导致性能下降——以及为什么在权衡之后仍然选择了惰性作为默认行为。

那个提问者读完之后回复了一句话,大意是如果教程里也能把这些权衡写进去就好了——他之前读过的所有材料都说Clojure使用惰性序列,好像这就是一个既成事实。他的困惑恰好指向了教学转译的核心问题。当设计决策从经过权衡的选择被转译为语言的既定特性时,学习者失去的不仅是一些历史信息。他们失去的是参与决策的能力。

如果你知道惰性求值是一个权衡的结果而不是一个自明的真理,那么当你在特定场景下发现惰性求值不适合时,你更有可能去寻找替代方案——比如使用transducer或者明确要求严格求值——而不是默默忍受或者抱怨语言设计。理解一个决策背后的理由,使你能够在理由不再成立时重新打开那个决策。但大多数教学材料没有传递这些理由。它们传递的是结论。这不是教学者的错。一本书或一篇博客文章的空间是有限的。你不可能在每个知识点上都重现当年的讨论过程。

而且大多数初学者在最初阶段并不需要知道惰性求值的权衡——他们只需要知道map返回的是一个可以遍历的东西就够了。问题在于当这些简化后的知识构成了社区中新成员的主要认知基础时会发生什么。一个可能的结果是:社区对什么是Clojure的集体理解开始从设计哲学滑向惯例集合。Clojure使用不可变数据结构不再意味着Clojure有一套关于时间、状态和并发的整体哲学,而是意味着你在写Clojure代码时应该用持久化集合。

Clojure是函数式的不再意味着Clojure鼓励一种特定的计算模型,而是意味着你应该尽量使用纯函数。这些惯例本身并没有错——它们是对哲学原则的合理应用——但当它们脱离了哲学根基之后,它们就变成了可以被选择性遵守甚至被挑战的规则。2020年前后的一些博客文章可以观察到这种变化。

有些文章讨论为什么在特定情况下使用可变局部变量或者何时打破函数式风格。这些文章的作者通常是经验丰富的开发者,他们的观点往往有合理的实践依据。

但他们的论证方式:他们不再诉诸Clojure的设计哲学来支持自己的选择,而是诉诸实用主义——这里用可变变量更快、这段代码用命令式写法更清晰。哲学讨论被绕过而不是被反驳。这与早期社区的讨论风格形成了鲜明对比。

在2010年左右,如果有人提议在某个场景下使用可变状态,讨论通常会回到Hickey关于状态和标识的区分上:你需要的真的是可变状态吗?还是你需要的是随着时间推移改变其值的标识?

这种讨论不一定会阻止使用可变状态——实际上Clojure本身就提供了atom、ref等引用类型来管理这种需求——但它迫使人们在使用之前先想清楚自己在做什么。当教学转译把哲学讨论压缩成实践指南之后,后来的开发者就失去了进行这种思考的语言工具。他们仍然可以写出好的Clojure代码——实用主义指导下的代码往往也是好代码——但他们写代码的方式与他们在另一种语言中写代码的方式之间的差异变小了。Clojure变成了工具箱中的另一个工具,而不是一种不同的思考方式。

这把我们带回到本章开头的那个引语。九二共识的重述史提供了一个观察共识变形的有用类比。

1992年香港会谈之后,海协会和海基会达成的共识是以口头方式各自表述的——双方都表示坚持一个中国原则,但对一个中国的含义各自保留解释空间。这种模糊性是共识得以达成的条件。

但随着时间的推移,不同的政治力量开始对这个共识进行转译和重述。每一次重述都试图把它变得更清晰、更确定、更容易向各自的受众传达。

在这个过程中,原始共识中的模糊性——那种使得共识成为可能的弹性空间——被逐步压缩了。2019年1月16日,中共中央台办发言人马晓光说明,海协会与台湾海基会1992年经由香港会谈及其后函电往来,达成了各自以口头方式表述海峡两岸均坚持一个中国原则的共识。二月二十七日,另一位发言人安峰山进一步说明,双方都是以两岸共同努力、谋求国家统一作为前提。

这些表述都是准确的——它们忠实地转述了当年达成的共识内容。但它们也做了一件事:把口头表述变成了书面表述,把各自表述变成了统一措辞。这不是篡改,而是转译的必然效果——当你把一种口头传统写成文字时,你必须选择一个版本。

Clojure社区的教学转译经历了类似的过程。早期教学传递的是一种带有所有微妙性和内部张力的活知识——就像口头表述的共识一样,它允许不同的理解共存于同一个框架内。后来的教学把这种知识压缩成了更清晰、更易于传播的形式——就像书面重述的共识一样,它消除了模糊性但也减少了弹性空间。

2021年秋天,在Clojure/conj会议上出现了一个有意味的场景。一位年轻的开发者在演讲中展示了他公司使用Clojure构建的系统架构。他的演讲清晰而有说服力,代码示例干净利落。

在问答环节,有人问他为什么选择Clojure而不是其他函数式语言。他的回答大致是喜欢它的简洁语法和强大的并发支持,而且社区很好。这是一个诚实的回答。但它也是一个让人感到某种不安的回答。

简洁语法和强大的并发支持——这些是Clojure的特性列表中的条目,它们也是Scala或Elixir或Kotlin的特性列表中可以找到的条目。社区很好——这当然是真的,但它没有触及任何关于Clojure独特之处的东西。我没有在问答环节追问什么——那不是一个适合深入讨论的场合。但我后来在想,如果这个问题提给2010年的Clojure用户,他们会怎么回答。

我猜想他们会谈到不可变性如何改变了他们对程序设计的思考方式,或者谈到REPL驱动的开发如何让他们与代码建立了一种不同的关系,或者谈到宏系统如何让他们能够以其他语言难以实现的方式表达抽象。这些回答不一定是更好的回答,但它们指向的是更深层的东西:不是Clojure有什么特性,而是Clojure让你成为了什么样的程序员。

教学的转译不仅改变了知识的内容,也改变了知识的形式。当Clojure被呈现为一组特性和最佳实践的集合时——即使这些特性和实践都被准确地描述了——它就从一种思维方式变成了一套工具。工具是有用的,但工具是可以被替换的。如果另一个工具提供了类似的特性组合和更好的性能或更大的生态系统,换工具就是合理的。

思维方式则不同——它塑造了你理解问题的方式,它变得更难替换但也更有价值。这就是教学转译带来的隐性风险:它让Clojure更容易被采用,但也让它更容易被放弃。回到那本入门书籍的版本演变。

《Clojure Programming》后来的印刷版在实用性上毫无疑问是进步了。更多的代码示例、更清晰的决策指南、更好的组织结构——这些都是真正的改进。

但如果你比较第一版和后来版本的引言部分,你会发现一个微妙的变化。第一版的引言中有这样一段话:Clojure不是试图在Java虚拟机上重新实现一种熟悉的语言;它是关于重新思考在拥有持久化数据结构、软件事务内存和宏系统的情况下,编程可以是什么样子。后来版本的引言保留了这段话的前半句——Clojure不是试图在Java虚拟机上重新实现一种熟悉的语言——但后半句被修改了:它是关于利用JVM生态系统的同时提供函数式编程的优势。

这两句话说的都是真的。但它们指向了不同的方向。第一句话邀请读者去思考编程可以是什么样子——这是一个开放的问题。第二句话告诉读者Clojure提供函数式编程的优势——这是一个已经完成的命题。这不是编者的疏忽。这是一个有意识的选择。

后来的版本面向的是一个更大的市场——不只是那些对重新思考编程感兴趣的人,还包括那些只是想要函数式编程的优势的人。从出版的角度看,这个选择完全合理。

但从知识传统的角度看,每一次这样的选择都在改变什么是Clojure这个问题的答案。当足够多的新成员通过转译而非原始文本学习语言时,共识的基础便从共同的理解转向了共同的习惯。这不是一种突然发生的断裂,而是一种缓慢的沉积过程——就像河床上的淤泥一层一层地堆积,最终改变了河道的走向。

2023年初,Clojure邮件列表上出现了一封特别的邮件。发信人是一个2015年左右加入社区的开发者。他在邮件中说,他最近重新读了Rich在2009年写的那些关于状态和标识的文章,发现他过去八年写Clojure的方式其实一直在误解核心观点——他一直把atom当成线程安全的变量在用,从来没有真正理解过为什么Hickey说变量这个词本身就是问题的根源。这个开发者的诚实是令人钦佩的。但更回复中的反应。

几位资深成员表示他们也有类似的经历——他们在使用Clojure多年之后才真正理解某些设计决策背后的推理。一位核心贡献者在回复中写道:文档和教程可以告诉你如何使用某个特性,但要理解为什么这个特性以这种方式存在,你通常需要回到原始的讨论中去。

这封邮件和它的回复揭示了一个事实:教学转译带来的知识稀释是可逆的——至少对某些人来说是可逆的。那些愿意花时间回到原始文本的人仍然可以找到那些没有被简化过的思想。但问题在于:有多少人会这样做?

更根本的问题是:当一个社区的大部分成员没有这样做时,社区的理解究竟意味着什么?这就是教学者的转译留下的遗产与变形。它让Clojure传播到了原本无法到达的地方——这是遗产的部分。它也在传播过程中改变了被传播的东西——这是变形的部分。

这两者无法分开。作为第一种同时具备词法作用域和尾调用优化能力的Lisp方言,Scheme将函数式编程的辐射力带向了更宽广的舞台,促使更多编程语言社区开始接触这些理念。

这段历史提供了一个比较的视角:当一种思想的传播范围扩大时,它不可避免地会被简化和改编以适应新的受众。Scheme的设计者们可能从未预料到lambda这个词会以何种方式进入主流编程词汇——不是作为lambda演算的实现细节,而是作为匿名函数的同义词。这种简化使得函数式编程的思想得以传播,但也使得那些思想中最激进的部分——比如通过lambda演算来理解计算本身——在传播过程中被淡化。

Clojure的教学史遵循了同样的模式。早期教学者传递的是一套完整的思想体系——关于时间、状态、标识和并发的整体哲学。后来的教学者把这套体系分解为可单独消费的知识单元:不可变数据结构是一个特性,惰性求值是一个特性,宏是另一个特性。

这种分解使得每个单元都更容易被学习和使用,但也使得单元之间的联系变得不那么可见。2024年夏天的一封邮件为这个故事提供了一个开放的结尾。

一个相对新的社区成员出现在Clojure邮件列表上,他说他正在做一个关于Clojure教学材料的调查项目。他想知道:如果社区只能向一个完全的新手推荐一份学习材料,那应该是什么?他收到的回答五花八门:有人推荐官方指南,有人推荐某本特定的书的最新版本,有人推荐一系列博客文章的组合阅读顺序,还有人建议直接从REPL实验开始而不读任何教材。

没有共识。这也许是最诚实的答案了。经过十五年的教学转译和知识变形,如何学习Clojure这个问题已经没有一个单一的正确答案了。不同的路径通向不同版本的Clojure——有些更接近原始的哲学文本,有些更接近实践惯例的集合,有些则混合了不同时代的转译层。选择哪条路径取决于你想要什么。但这也意味着,什么是Clojure这个问题同样没有单一的答案了。

它曾经有过——或者至少有一个相对集中的理解范围——在那个创造者和教学者还是同一群人的时代。

现在它有了多重答案,每一重答案都对应着一个不同的学习路径、一套不同的教学材料、一种不同的知识传统。这不是悲剧。这是任何活着的知识传统必然走向的状态。

但它也是一个需要被看见的状态——因为那些安静的变形正在以不易察觉的方式重塑着社区继承的东西。那些离开的人带走了什么?那些新来的人学到了什么?那些被简化掉的微妙之处去了哪里?

这些问题不会自动出现在社区议程上,但它们会一直存在,就像地基中那些缓慢扩大的细微裂缝,等待着被注意或被忽视。教学者的每一次转译都是一次交易——用精确性换取可接近性,用深度换取广度,用哲学换取实践。每一笔交易在当时看来都是合理的,甚至是必要的。但这些交易的累积效应是:社区所继承的那座建筑正在以一种不可见的方式改变着它的结构。

它看起来还是同一座建筑——同样的语法、同样的库、同样的邮件列表——但住在里面的人理解它的方式已经不同了。这就是遗产正在变成的东西。