第 4 章
文档的褶皱
Grimoire项目的GitHub仓库里,最后一次提交停留在2014年3月15日。提交信息只有一行:“Fix compatibility with Clojure 1.6”。在那之后,再没有新的代码推送到这个仓库。但仓库本身没有被删除,Issue列表还开着,偶尔有人路过,留下一条新的评论——“这个项目还维护吗?”——然后被沉默吞没。
这份沉默是一个入口。它通向的不是一个失败项目的记录,而是Clojure社区如何处理知识传递这个更根本的问题。Grimoire的停滞不是孤立事件。它和2012年那份命名指南的停止更新,和邮件列表上反复出现的同一个问题,和那些过时但未被删除的博客教程,属于同一种现象。这些现象共享一个结构性的原因:在Clojure社区的治理体系中,文档的位置是模糊的——它不属于语言核心的契约部分,也不完全属于社区的自发实践,而是悬浮在两者之间,像一个没有人正式认领的灰色地带。
要理解这个灰色地带的形成,需要回到更早的时间。2009年10月,Clojure发布了1.0版本。在发布公告中,里奇·希基列举了语言的核心特性:不可变数据结构、软件事务内存、与Java的无缝互操作、Lisp宏系统。文档不在这个列表里。这不是疏忽——希基在后续的邮件讨论中明确表达过他对文档的看法:语言的核心文档应该精确描述语义契约,而不是提供教程或使用指南。语义契约是语言设计的一部分,需要与语言本身同步维护;教程和使用指南则是社区的事,应该由使用语言的人来编写和分享。这个看法在当时没有引起争议。2009年的Clojure社区很小,早期的采用者大多是有经验的Java或Lisp开发者,他们习惯从docstring和源码中推断用法,习惯在邮件列表上提问和回答,习惯自己写博客来分享心得。
对于这个群体来说,希基的分工是合理的:核心团队负责语言的精确性,社区负责知识传播的丰富性。但合理性不能替代可行性。
分工的前提是社区有足够的能力和意愿来承担知识传播的责任。这个前提在2009年成立,在2010年成立,但到了2012年,随着Clojure用户群体的扩大和多样化,开始出现裂痕。裂痕最早出现在邮件列表上。2012年春天,Michael Fogus——《The Joy of Clojure》的合著者之一——在邮件列表上发了一个观察。他说他注意到一个问题:越来越多的人在邮件列表上提问,但回答的人似乎在减少,或者至少在变得疲惫。同一个问题——关于reduce的用法,关于lazy-seq的行为,关于macro的展开时机——被问了多次,每一次都有人回答,但回答的质量和耐心程度在下降。Fogus不是一个普通的社区成员。他从2009年就开始使用Clojure,参与过多个开源项目,在邮件列表上回答过数百个问题。他的观察不是抱怨,而是诊断。
他认为问题的根源不在于回答者不够热心,而在于知识的传递方式:邮件列表是一个对话媒介,擅长处理一次性问题,但不擅长积累和系统化知识。每一次回答都只是对提问者一个人的回应,而不是对社区知识库的补充。当同样的问题被第十次提出时,回答者不是在教导第十个新人,而是在重复自己已经说过九遍的话。Fogus建议社区考虑创建一个结构化的文档站点,把常见问题的答案系统化地整理出来。这个建议得到了支持,但没有转化为行动。原因不是缺乏意愿,而是缺乏执行机制。创建一个文档站点需要有人负责搭建基础设施、组织内容、维护更新。这些工作都需要时间,而在Clojure社区中,每个人的时间都是自愿贡献的。有足够空闲时间的人不一定有足够的技术能力,有技术能力的人不一定有足够的空闲时间,两者都有的人可能正在维护自己的开源项目,没有额外的精力来承担社区级的基础设施建设。但这个需求没有消失。
它只是暂时潜伏,等待一个触发点。触发点发生在2013年初,以两份统计数据的形式出现。第一份数据来自Andy Fingerhut。他花了几个周末的时间,遍历了Clojure邮件列表从2008年至今的所有存档,手动标记了每一个提问帖的主题。他统计了不同主题的出现频率,结果很清楚:最常被问到的问题不是语法疑难,不是并发模型,不是宏的编写技巧,而是“这个函数到底怎么用”——一个文档问题。具体来说,被问得最多的是reduce、mapcat、group-by、update-in这几个函数。它们的docstring在技术上是准确的,但对于不熟悉函数式编程范式的开发者来说,这些描述不足以让他们理解何时以及为何使用这些函数。第二份数据来自2013年的社区调查。这项调查由Chas Emerick发起,收集了1060名受访者的回答。
调查显示,47%的受访者在使用Clojure的同时也使用ClojureScript,这意味着相当一部分用户正在处理前后端集成的复杂场景,对文档的需求超出了单一语言的范围。调查还问了一个关于文档满意度的问题——虽然具体数字需要去原始报告中核实,但Emerick在总结中提到的趋势是清楚的:文档被认为是Clojure生态系统中最需要改进的领域之一。Fingerhut把统计结果发到了邮件列表上,附上了一个建议:有没有可能创建一个社区维护的文档站点,专门收集使用示例?回应来得很快,方向却不一致。Zachary Kim提出了Grimoire计划。他的设想是自动化:从Clojure源码中提取docstring,从GitHub上抓取公开项目中使用该函数的代码片段,将两者交叉引用,生成一个自动更新的文档站点。
这个设想的吸引力在于,它不需要持续的人力投入——一旦系统搭建好,就可以自动运行,理论上可以随着语言和社区项目一起演化。
Kim有实现这个设想的技术能力。他花了一个周末写出了原型,然后在一个月内迭代出了功能完整的版本。Grimoire的界面简洁但功能齐全:左侧是命名空间和函数的树形列表,右侧是内容面板。点击一个函数,首先看到的是从源码中提取的docstring和参数列表,然后是“社区示例”区域——从GitHub上抓取的代码片段,每段都标注了来源项目。底部的“相关函数”部分通过分析调用关系自动生成。邮件列表上的反应是热烈的。有人称它“正是我们需要的”——官方文档告诉你函数是什么,这个告诉你函数怎么用。Kim在接下来的几个月里持续改进,增加了版本切换功能、示例投票机制、以及一个简单的搜索界面。到2013年夏天,Grimoire已经覆盖了Clojure核心库的大部分函数,收集了数百个社区示例。
但一个根本性的问题从一开始就埋下了。Grimoire的核心功能——从源码中提取docstring——需要解析Clojure编译器使用的元数据格式。
这种格式会随着Clojure版本的变化而改变。每次Clojure发布新版本,Kim都需要手动更新解析器,重新提取所有函数的文档,然后检查交叉引用是否仍然有效。自动抓取的社区示例也有问题:GitHub上的项目会更新、重构、甚至删除,导致链接失效或示例过时。筛选高质量示例需要人工判断,而人工判断需要时间。
Kim的提交记录清晰地勾勒出了精力耗尽的过程。2013年3月,127次提交;4月,89次;5月,64次;6月,31次;7月,12次;8月,3次。
然后是一段空白。9月有一次提交,修复了一个链接失效的问题。10月没有提交。11月有一次合并请求,来自另一个开发者。12月没有提交。
2014年1月,Kim在Grimoire的Issue列表上发布了一条留言,说明自己无法继续维护这个项目,希望有人接手。等了两周,没有正式响应。有几个开发者表示愿意帮忙修复特定的bug,但没有人愿意承担整个项目的维护责任。
最后一次提交就是2014年3月的那一条,修复与Clojure 1.6的不兼容。之后,仓库沉寂了。Grimoire的兴衰只用了一年多的时间。它的失败不是技术上的——系统运行良好,代码质量不低——而是治理上的。Grimoire是一个人的项目,它的命运绑定在一个人身上。当那个人的精力耗尽时,项目就死了。Clojure社区在2013年还没有机制来识别这种关键基础设施并为其提供持续支持。那时还没有Clojurists Together这样的资助组织,没有明确的“社区维护”概念,没有将个人项目转化为集体责任的流程。
但如果仅仅把Grimoire的故事看作一个失败案例,就会错过它揭示的更深层的东西。Grimoire的“自动化”设想本身,反映了一种对文档问题的特定理解:文档是信息的聚合,问题在于如何高效地聚合和展示信息。这种理解遗漏了一个维度——文档不仅是信息的聚合,也是共识的体现。
一个函数的“正确用法”不是一个可以通过算法从代码中提取的事实,而是社区在使用过程中逐渐形成的判断。这个判断可能随着时间的推移而变化,可能在不同的人群中存在分歧,可能需要协商和争论才能稳定下来。Grimoire试图用技术手段解决一个需要治理手段的问题。它的失败不是个人的失败,而是揭示了自动化在处理知识共识问题上的根本局限。
几乎与Grimoire同时启动的另一个项目,走向了不同的方向,而且存活了下来。2013年夏天,Sean Corfield创建了ClojureDocs。与Grimoire不同,ClojureDocs不试图自动抓取任何东西。它的设计原则很简单:每个核心函数都有一个页面,页面顶部是官方docstring——手动复制粘贴,而不是自动提取——下方是社区贡献的示例。任何人都可以提交示例,其他用户可以投票——点赞有用的示例,点踩无用或错误的示例。示例按投票数排序,最有用的浮到顶部。这个设计解决了好几个问题。
手动复制docstring虽然看起来笨拙,但避免了自动提取带来的维护负担——docstring变了就手工更新一次,不需要维护一个复杂的解析器。社区贡献示例的模式将内容生产分散到所有用户身上,不会因为一个人的离开而瘫痪。投票机制提供了一种轻量级的质量控制手段,不需要专门的编辑团队来审核内容。
但更重要的是,ClojureDocs的设计隐含了一种对文档问题的不同理解。文档不是信息的聚合,而是知识的沉淀——而知识沉淀的核心机制不是自动化,而是参与。通过让用户贡献示例并投票,ClojureDocs实际上在创建一个微型治理系统:它不预设哪些用法是“正确”的,而是让社区通过参与来形成共识。一个示例被顶到顶部,不是因为它是权威来源发布的,而是因为足够多的人认为它有用。这个“有用”可能包含多个维度——示例代码简洁、应用场景常见、解释了容易混淆的细节——但关键的是,这些维度不是由系统预先定义的,而是由投票者的集体判断体现的。
到2013年底,ClojureDocs收集了超过两千个示例,覆盖了大部分核心函数。贡献者不是一小群热心人,而是一个分布式的网络:有人贡献了map的五个不同用法,有人为reduce写了从简单累加到复杂数据转换的渐进式示例序列,有人专门补充了宏的用法说明——那是文档中最薄弱的环节之一。ClojureDocs的存活证明了社区参与模式在文档领域的可行性。
但它也暴露了这种模式的内在局限。投票机制虽然筛选出了最有用的示例,但引入了一种隐性的偏见:早期贡献者拥有先发优势。一个2013年提交的示例,经过一年的投票积累可能获得几十个赞,而一个2014年提交的、质量可能更高的新示例需要很长时间才能追上。这意味着一些页面上展示的“最佳示例”反映的是2013年的实践方式,而不是当前的最佳实践。
Clojure语言本身在进化,社区对惯用法的理解在深化,但投票机制缺乏时间维度——它无法区分一个示例是“曾经很好”还是“现在仍然很好”。
更深层的问题在于示例的性质本身。一个示例展示的是“怎么用”,但它不可避免地隐含了“应该怎么用”的判断。当两个人对“应该怎么用”有不同理解时,谁的示例会被投票到顶部?2014年,关于atom的使用方式出现了一场安静的争论。一方提交了使用atom管理全局状态的示例,认为这是介绍atom的最直观方式;另一方认为这鼓励了不良实践,atom应该主要用于局部可变状态,全局状态应该用更专门的构造来管理。争论没有升级为冲突——Clojure社区的讨论文化倾向于避免正面对抗——但它体现在了投票行为上:双方开始给自己的偏好点赞,给对方点踩。最终胜出的是那个更早提交的示例,不是因为它更好,而是因为它积累了更长的投票时间。
这不是ClojureDocs的失败,而是任何依赖投票机制的系统的固有局限。投票可以筛选质量,但无法解决争议;它可以反映共识,但无法生成共识。
当社区对某个问题没有共识时,投票机制要么放大偶然性(先发优势),要么暴露分歧(票数接近),但无法创造共识本身。而共识的创造——通过讨论、协商、妥协和权威判断——恰恰是核心团队在docstring层面所做的事,只是他们选择将这种共识的适用范围严格限定在语义契约的边界之内。
这引出了问题的核心:为什么核心团队选择不扩展文档的边界?答案需要从Clojure的设计哲学中寻找,但它不是明面上的哲学声明,而是体现在一系列具体决策中的隐性原则。2010年,有人在邮件列表上提议为clojure.core中的每个函数添加使用示例。提议者认为,既然社区已经积累了大量关于函数用法的知识,把这些知识整合进官方文档可以大幅降低新人的学习成本。提议得到了不少支持,但被核心团队拒绝了。拒绝的理由不是反对示例本身,而是反对将示例纳入官方文档的范畴。
核心团队的立场是:docstring是语言契约的一部分,它承诺的是函数的行为——给定这些参数,返回这个类型的结果,产生这些副作用。这个承诺需要精确,因为它直接影响代码的正确性。使用示例不是契约,而是建议;建议可以变化,可以争议,可以因上下文而异。将建议与契约混在一起,会模糊语言定义的边界,让使用者难以区分“这个函数保证做什么”和“这个函数通常被用来做什么”。
这个区分在理论上是有道理的。但在实践中,它创造了一个空白地带。对于有经验的开发者来说,从契约到用法的距离很短——他们可以通过阅读docstring和函数签名推断出典型用法,可以通过实验来验证自己的理解。对于缺乏函数式编程背景的开发者来说,从契约到用法的距离可能很远——他们需要一个中间步骤,一个从“函数做什么”到“我可以用它做什么”的桥梁。在2010年到2012年间,这个桥梁主要由邮件列表和博客提供。但这个桥梁有一个隐含的成本,直到用户群体扩大后才变得明显。
2013年,Clojure用户群体的构成开始发生变化。根据社区调查,早期的Clojure用户主要来自Java背景,有五年以上的编程经验。他们选择Clojure是因为它提供了Java生态中缺乏的函数式编程能力和并发模型。这个群体有一个特点:他们已经在Java生态中学会了如何学习——他们知道如何阅读API文档,如何在邮件列表上提问,如何从源码中推断行为。
他们需要的不是教程,而是精确的语义描述。但从2013年开始,新用户中来自Ruby、Python、JavaScript等动态语言社区的比例上升。这些开发者被Clojure的函数式特性和并发模型吸引,但缺乏JVM生态和Lisp家族的知识背景。他们习惯的文档形式与Clojure提供的不同:Ruby有丰富的官方教程和社区维护的指南,Python有详尽的官方文档和大量的第三方教程,JavaScript有MDN这样的权威参考。
当他们来到Clojure社区时,面对的是精确但简短的docstring,和一个期望他们自己去搜索邮件列表和博客的隐含假设。这个假设对于新用户来说并不总是成立。不是因为他们缺乏能力,而是因为他们缺乏导航信息丛林所需的地图。一篇2010年的博客教程可能仍然有效,也可能已经过时;一个邮件列表讨论可能包含了正确的答案,也可能反映了已被修正的误解。辨别这些需要经验,而经验正是新人所缺乏的。
2015年,这种张力达到了一个临界点。根据那年的社区调查——2445名受访者参与——ClojureScript的使用率达到了66%。新用户的比例继续上升。邮件列表上出现了越来越多的问题,不是关于特定函数的用法,而是关于“如何组织一个Clojure项目”、“何时使用宏而不是函数”、“如何处理错误”——那些介于语言语义和软件设计之间的问题。这些问题更难回答,因为它们涉及判断和权衡,而不是事实和规则。
回答它们需要更多的时间和精力,而愿意投入这些时间和精力的社区成员是有限的。一个可观察的现象是:邮件列表上的资深成员开始表现出疲惫。同样的问题被问了第十遍、第二十遍,每一次都需要有人耐心地解释同样的概念。有些人不再回复,有些人开始简短地回答“请搜索邮件列表存档”,有些人贴出之前讨论的链接。
这并不是不友好——Clojure社区一直保持着相对友善的讨论氛围——但重复劳动消耗了志愿者的精力,而精力是有限的资源。2015年秋天,Daniel Compton创建了“Clojure Guides”仓库,试图用一种不同于博客和邮件列表的方式来填补文档空白。他的想法是:将指南放在GitHub上,使用Markdown格式,接受Pull Request,允许多人协作维护。这样就不会重蹈博客教程过时后无人更新的覆辙。Compton写了几篇初始指南,包括一篇关于Leiningen项目结构的详细说明和一篇关于core.async使用模式的介绍。
他在邮件列表上宣布了这个项目,最初的反响不错,有人提交了修正拼写错误的PR,有人补充了测试相关的章节。但六个月后,这个仓库也陷入了停滞。原因不是Compton失去了兴趣——他仍然在维护——而是贡献的质量和一致性难以保证。一个人写的指南可能与另一个人写的指南在风格、深度、预设读者水平上完全不同。协调这些差异需要编辑工作,而编辑工作需要时间、判断力和权威性——这些都是志愿者项目难以持续提供的。
Compton在2016年初写了一份项目状态更新,措辞谨慎但意思清楚:在组织贡献方面遇到了挑战,不同指南之间的连贯性不够理想,需要更多的编辑工作来确保质量一致性。这些尝试的轨迹——Grimoire、ClojureDocs、Clojure Guides——勾勒出了一个模式。社区一次又一次地试图填补官方文档留下的空白,每一次都取得了一定程度的成功,但每一次都遇到了同样的结构性限制:志愿者精力有限,质量一致性难以保证,长期维护缺乏制度支撑。
这些不是个人能力的问题——Andy Fingerhut、Zachary Kim、Sean Corfield、Daniel Compton都是有能力的开发者——而是治理结构的问题。Clojure社区在文档领域的治理,依赖于分散的个人自愿贡献,缺乏协调机制、持续性保障和质量控制基础设施。
但把这个模式简单地定性为“失败”是不公平的,也是不准确的。从2008年到2015年,Clojure社区在没有大厂支持的情况下,创造了一个可用的、丰富的、持续演化的文档生态。数以千计的开发者通过这个生态学会了Clojure,用它构建了生产系统,其中一些人后来成为了核心贡献者。ClojureDocs至今仍在运行,每天有数百人访问它的页面。邮件列表存档中积累的知识是任何单一文档站点都无法比拟的。那些过时的博客教程,尽管不再准确,曾经在当时帮助了一代人入门。真正的代价不是文档的缺失,而是文档褶皱中的摩擦——那些新人在学习过程中必须独自穿越的间隙。
2015年底,有人在邮件列表上发了一个帖子,标题是《为什么我放弃了Clojure》。发帖者是一个有八年Python经验的开发者,花了三个月时间学习Clojure,最终决定回到Python。他的理由不是语言本身——他说他喜欢Clojure的语法和并发模型——而是学习曲线的陡峭程度。文档告诉他pmap是什么,但没有告诉他什么时候不应该用它。他花了两个星期才从一篇2012年的博客文章里找到答案,而那篇文章的作者已经不再使用Clojure了。
这个帖子引发了一场讨论,持续了数周。在讨论中,一个来自核心团队成员的回复特别值得注意。他没有否认问题的存在,也没有承诺改进,而是解释了核心团队对文档边界的理解。他说的意思是:他们提供精确的工具和清晰的契约,如何组合这些工具取决于具体的应用场景,他们不想通过规定“正确”的用法来限制开发者的选择。这是一个诚实的回答,但它也揭示了一个根本性的张力:工具制造者的哲学与工具使用者的需求之间的张力。
希基和他的核心团队把自己视为工具制造者——他们提供的是原材料(语言原语)和精确的规格说明(docstring),把组合和使用的方式留给工匠自己探索。但对于许多使用者来说,他们需要的不仅仅是工具和规格,还有使用工具的技艺——那些介于理论和实践之间的、通常通过师徒关系传递的隐性知识。在没有师徒关系的开源社区中,文档承担了部分师徒的功能。但文档是一种有限的媒介:它可以记录知识,但难以传递判断;它可以展示示例,但难以培养直觉;它可以回答问题,但难以在问题被提出之前就引导学习者走向正确的方向。这些局限不是文档本身的缺陷,而是知识传递的本质决定的——有些东西只能通过实践、反馈和反复试错来习得,无法被任何形式的文档完全替代。
2016年初发生了一件小事,它本身不重要,但作为一个象征值得记录。一个开发者发现clojure.core命名空间中有一个函数的docstring包含一个拼写错误——“occurence”应该是“occurrence”。
他提交了一个补丁。补丁被接受了,但在审查过程中,另一个核心开发者注意到这个函数的docstring不仅有一个拼写错误,而且整个描述可能产生误导——它在技术上准确但在教学上有害,因为它暗示了一种不推荐的用法模式。于是开始了一场讨论:是只修复拼写错误,还是重写整个docstring?如果重写,新的措辞应该精确到什么程度?是否应该添加一个使用建议?讨论持续了几天,最终的决定是:只修复拼写错误。重写docstring涉及判断“什么是推荐的用法”,而核心团队不想在docstring中做出那种判断。那个拼写错误被修复了。误导性的描述保留了下来,继续存在于每一个Clojure发行版中,等待着下一个读到它的开发者产生困惑,然后去邮件列表提问,然后得到一段解释,然后那段解释沉入存档,等待下一次被搜索捞起。这个过程就是文档褶皱的日常运作。
它不是戏剧性的失败,而是一种持续的、低强度的摩擦——每一个新人都要独自穿越同一片信息丛林,踩过同样的泥坑,被同样的树枝绊倒。有些人走过去了,有些人回头了。那些走过去的人中,有一部分后来成为了指路者,在邮件列表上回答下一个新人的问题,完成这个循环。这个循环本身,就是Clojure社区知识传递的隐性宪法——没有被写进任何文件,没有被任何人正式批准,但被每一个参与其中的人默默地执行着。它的有效性取决于一个前提:走过去了的人愿意留下来指路。但指路也需要精力,而精力是有限的。当新人的数量超过指路者能够承受的阈值时,循环就会开始松动——指路者变得疲惫,变得简短,变得不再回应,然后新人无法走过去,他们回头,循环的输入减少,循环本身开始萎缩。
这并非遥不可及的风险。它是那份命名指南的停止更新、Grimoire的沉寂、邮件列表上资深成员的疲惫所共同指向的同一个问题:依赖志愿者精力的知识传递机制,在没有制度支持的情况下,其长期可持续性是不确定的。
这个不确定性不是要否定社区已经取得的成就——那些成就真实而持久——而是要指出,在一个不断扩大的社区中,维持隐性宪法的执行需要不断投入新的精力,而精力的供给不是无限的,也不是自动的。它需要被识别、被组织、被支持——这些正是Clojure社区在文档领域尚未充分发展出来的能力。