普通视图

发现新文章,点击刷新页面。
昨天以前極客死亡計劃

工作、消费主义和新穷人

2026年6月16日 15:30

一本经济和政治相关的社会科学书籍,很薄,还算易读。书从工作伦理和消费美学出发,浅析了工作、生产和消费在社会不同阶段的意义,又谈到如今穷人的处境,他们是如何被福利国家救助、被当作生产力后备军,然后又是如何被消费主义社会抛弃、被当作不合格的消费者的。看起来是对工作和消费主义的解析,不过主要的落脚点还是在穷人群体身上,最后呼吁人们用「工艺伦理」代替工作伦理。

不过我其实不太明白工匠伦理(将社会从对计算、可预测性、经济增长和市场的追求中解放出来,去关注匠人之道)对书中描绘的被社会抛弃的穷人群体有什么用处,尽管它的确是我也很期待的愿景。整本书的结构有些散,章节乃至段落之间的关联不够紧密,前几章又有大量对其他哲学家、思想家观点的引用和重新叙述。总体上是一本内容尚可,但阅读体验一般的社科书籍。

由于写得散,我读得也比较散,有从头到尾读完,没有跳读,但没有仔细推敲每个段落和概念的语义,所以书评也写得相对短些,只挑我印象深刻的写写。

工作伦理与消费美学的双重影响

工业化阶段,在工厂内部上演了一场道德改革运动,人们希望通过某种手段将散漫、慵懒的工人动员起来,由此诞生了「工作伦理」。其内容大概是,人必须工作创造价值,不工作就会堕落,是不正常的;工作必须全情投入、奉献,工人应该追求更好的工作表现。

然而,这些追求,是过去的工匠在自己掌控工作时,自然而然地表现出来的。

工作伦理诞生于让工人适应工厂生活的需求,由此,工匠变成了工人,工匠的那些坚持被视为执拗,个性不被欣赏,没有容身之处。因为个性和创意是不可计算、不可预测的,工作伦理希望个人的情感与他们的行为无关,以便生产结果可控。

我们平时赞赏的美德,在奴隶身上就成了罪恶。

此外,另一个社会因素也支持了工作伦理运动的胜利,也就是穷人救济组织的出现。对穷人的救济必须是最低限度的,救济院的生活必须被描述为难以忍受的、堕落的、深陷赤贫的,这样其他穷人才会愿意工作,只要有一份能获得最微薄工资的工作,有工资的穷人的生活就比那些依靠救济的穷人的生活更诱人。

在生产者社会,工作也是个人身份的核心。“一旦确定了工作类型和职业规划,其余的事情就水到渠成,需要做什么也基本确定下来。”工作也成了社会秩序的基准之一,那些失业者、体弱多病和精神障碍者,都是令人担忧的威胁和障碍,因为不工作是失常的。工作伦理带来的新秩序,也通过父权制进入家庭,将规训的压力传导到圆形监狱无法触及的人群。工作伦理的推动者也是家长权力的倡导者。

这些都是道德层面的规训,不过在进入消费者社会后,纯粹的道德规训失效了:

与其宣扬努力工作通向道德高尚的生活,不如告诉大家这是赚取更多金钱的手段。

在消费者社会中,经济增长不再主要取决于生产力,至少人们认为,它取决于消费者的热情和活力。技术进步和生产活动的再造(reengineering)使得人们对工人的需求变得越来越少,解雇和重组在制造出新穷人的同时,又使得幸存的消费者能赚到更多的钱。显然,消费者社会更偏向这些更具消费活力的消费者。

生产是集体的,而消费是孤独的个人选择。工作伦理逐渐被消费美学取代,人们的身份也不再由职业决定(因为能干一辈子的工作不复存在),而是由消费和选择决定。一个人是什么,取决于他消费什么。消费主义强调个人选择,选择的标准就是美学标准,人们用美学评判商品,也用来筛选工作。尽管职业是平等的,但有些岗位是缺乏美的,不能帮助人实现使命——尽管有使命感的工作就是少数人的特权。

具有使命感的工作,成为少数人的特权,成为精英阶层的特有标志。其他人只能敬畏地远观、艳羡,只能通过低俗小说和肥皂剧来体验。

谈到小说和肥皂剧,我不禁怀疑短视频和社交媒体更大地满足了人们对使命感和消费美学的幻想,使得人们过上「具有美感」的生活的愿望变得越来越弱——某种程度上,也更好地服从了退居后台的工作伦理。

值得一提的是,消费者社会中,「无聊」这一情绪也愈演愈烈,人们总是追求新的刺激。无聊实际上同时具有阶级性和现代性,穷人不感到无聊,古人也不感到无聊,现代的中产者才无聊。“无聊是消费者社会特有的社会分层因素产生的心理学结果。”容许我跑个题,《 红与黑 》中的玛蒂尔德小姐作为贵族阶级,就常常受到无聊这一情绪的困扰,改编的音乐剧《摇滚红与黑》中,玛蒂尔德小姐第一次出场时的歌曲 Quel ennui 就是她在贵族聚会上反复吟唱:好无聊,好无聊…… 1

从生产者社会到消费者社会,可以发现穷人的处境随着经济增长变得更糟,而富人只会更富。经济发展使得工作岗位变得“灵活”,穷人没有工作,消费能力也降低,被消费者社会定义为「不合格的消费者」,被消费美学驱逐。

如今,工作伦理和消费美学共同影响着穷人(以及所有人)的处境。工作的重组和再造使得工作机会越来越少,无休无止的现代化使得前现代的生活方式基本消失(即,所有人都需要工作),人们被工作伦理要求去工作,却找不到工作,接着被消费美学抛弃,成为边缘群体。

穷人无法生产也无法消费,不被看见,也没有机会发声。

穷人,逐渐隐形的社会问题

在生产者社会,救济穷人是为了帮助他们“重新加入我们”,回归正常的、有工作的状态。如今这个想法已经不太现实了,世界上已经没有什么穷人的去处。在以前,人们可以把穷人“出口”到其他国家和地区,可现在,经济全球化和无休无止的现代化,使得这些“回收站”也找不到了。穷人成为底层阶级,并且再也无法跃升。穷人为了生存,被逼迫犯罪,于是监狱就成了新的“废品回收站”。

社会对穷人态度的转变可以说是大转弯。在以前,人们还会提出「福利国家」的概念,尽可能多地帮助那些无法获得经济收入的人。关于国家福利,微妙的点在于「是否有经济审查」和「被救助的对象是否值得救助」。

关于经济审查,一部分人认为福利援助不应该审查被援助者的经济情况,即便有车有房,遇到突如其来的经济困难,也应当得到救助。这看起来不太符合直觉,但一旦意识到「穷人得到的东西永远是最差的」,无审查的救助就会变得合理。如果福利只服务于穷人,那么富人就不会在乎,而穷人又没有机会发声,那么他们只会得到最差的援助。如果福利作为国家保险造福于所有人,任何人都可能在遇到困难时求助国家福利,那富人就愿意关心福利问题,穷人也能得到更好的救助。

可事实是,富人只希望自己能少交点税,关于保险,他们更愿意购买比国家保障更好的私人保险。福利国家的救助最终还是只作用于穷人,一减再减。

福利国家体现了工作伦理,那些支持者救助的是「有工作能力、能创造价值,但暂时失业的人」,也是以工作为中心的。不过后来人们意识到(至少相当一部分知识分子写到),给穷人太多的钱,他们只会去挥霍。富人忍受不了自己多交的税被拿取给不思进取的穷人挥霍。福利国家的理念也与消费主义背道而驰,福利是「被分配的」,而不是「自主选择的」,缺乏消费美感。

随着福利国家的衰败,穷人所带来的社会责任被不断向下踢皮球。各种类型的群体被逐渐归类为底层群体,他们被媒体渲染为不思进取、做出错误决定、自己搞砸了生活的失败者,罪有应得而且不值得拯救。此时工作伦理又发挥了作用,它把社会责任转变为了穷人的个人责任——如果要摆脱这种生活,就去工作。

由于福利机构无法承接,全球也没有别的地方供穷人生存,处在底层阶级的他们逐渐投向犯罪,然后被关进监狱,以国民安全为由安置、隔离他们。

把穷人塑造成“劳动力后备军”的工作伦理,在诞生时是一种启示,在死后却变成了一种掩饰。

不过说到底,穷人再怎么被知识分子塑造成不思进取、不值得拯救的败类,他们是有实际价值的:

每一个已知的社会都对穷人持有一种特有的矛盾态度,一方面是恐惧和反感,另一方面是怜悯和同情。这两种成分都不可或缺。前者允许在需要秩序维护的时候对穷人进行严厉的处理;后者强调了那些达不到标准的人的悲惨命运,由此让正常生活的人在遵守社会规范时遭遇的所有艰辛都变得微不足道。

即便是在这本关心穷人的书里,穷人也只是一个抽象概念,是一类被谈及但不被讲述故事的群体。我们或许能看到街头上的流浪汉,或许知道在某个角落里艰难生存的人的存在,但有谁关心他们的故事呢?

工作可以被解放吗?

书本的最后,作者回到最初的「工作」这一概念。

为了把工作从以市场为中心的计算和限制中解放出来,就必须以工艺伦理(ethics of workmanship)取代劳动力市场发展过程中形成的工作伦理。托斯丹·凡勃伦早就指明,“工艺本能”有别于工作伦理这个现代发明,是人类的自然倾向。人是具有创造力的生物。如果认为标价牌是区分工作与非工作、努力与懒惰的标准,那是对人类本性的贬低;如果认为没有收益,人们宁愿闲着,让自己的技能和想象力腐烂生锈,那是对人类本性的肢解。工艺伦理将恢复人类本能的尊严,恢复社会公认的意义。现代资本主义社会形成且根深蒂固的工作伦理却否认了这种尊严和意义。

这个愿景很美好,而且作者指出:人类以前也改变过,为什么不能再次改变?可「有潜力」不代表一定能做到,能做到也不代表会(在一定时间内)发生。鸡蛋之前也被煮熟过,为什么现在不能熟?——然而根本没有人生火烧水。作者的愿景并没有看起来可以执行的方案,仅仅是愿景而已。

我也希望工作可以不以市场为中心,那些不被计算、不可预测、不能量化的东西也可以立刻产生实际的价值,但有谁会买单呢?或许是既有的市场思维限制了我的想象力,我的确想不到社会能从什么地方入手,开始关注所谓的工艺伦理。工艺伦理看起来像是个人和企业内部可能会关注的东西,一部分企业可能很关注代码评审,另一部分可能希望员工同时操作五个编程智能体,不在乎代码质量疯狂产出。他们对外交付的成果可能有区别,但这些区别可能是消费者不在乎的。

或许问题的根源是社会跟不上科技进步、现代化和全球化的步伐。就像阿伦特在《人的境况》里复兴城邦制的愿景一样,城邦必须在较小型的社会中才能健康运作,那里人们能相互看见、倾听对方的思想,工匠精神或者说工艺伦理,也只有在小社群中才能可能被欣赏。随着经济规模的扩大,复兴工艺伦理听起来有点像是在工厂里当工匠,无人在意。

所以,求求你们,少生点吧!


  1. 再跑个题,关于「无聊」,我在 第 29 期周刊 (白日梦之死)和 第 41 期周刊 (“AI” 即将消灭孤独)中分享了有关应对「无聊」这一现代情绪的内容。 ↩︎

大脑充血 Vol.85

2026年6月15日 08:28

《稻草人周刊》更名为《大脑充血》,不过内容的变动不大,还是继承以前的编号。名字的灵感来源于我很喜欢的一张塔罗牌「 倒吊人 」。


如你所见,周刊的结构做了调整。所有技术相关的内容被移动到了名为「茶歇」的新栏目,里面包含着一些比较轻松的技术新闻,当然也有我的观点。原来的「连接」栏目更关注相对通俗的大众话题,也涉及人文社科。

原本只有一些零碎信息的「切片」栏目代替了我的社交媒体发帖,我按照时间顺序列出了我以前会发送到社交媒体上的内容。有些东西我其实仅仅是希望表达出来,不一定要立刻收到回应。周刊当成一个缓冲区,每周发布一次这些碎片内容,可以减少我对社交媒体的依赖。

另外,你可能知道,我这周屏蔽了 Matrix 和 Fediverse 的消息,以后很长一段时间都会这样。我想我可能只会每周或者每两周看一次那边的消息。如果需要联系,请给我发电子邮件。


止语

you seem pretty sad for a girl so in love music cover

you seem pretty sad for a girl so in love 专辑

Olivia Rodrigo

I’m a zombie in my body, I’m a train off of the track

I feel dirty, I feel rotten, and the colors are all flat

I’m a sad shell of a woman and I’ve got maggots for brains

——《maggots for brains》


连接

如何恰当地浪费一个上午

📜

I’ve heard that there is no virtue in rising before the sun unless you are harvesting olives or fleeing the authorities

我曾听说,除非你要收获橄榄,或者逃离权威,赶在日出前起床就不是美德。

生产力指南到处都是,或许应该停下来,读读这篇非生产力指南。恰当地浪费一个上午并不是慵懒,而是刻意的体验,感受身边微小的细节和时间像沙子一样流过指尖的愉悦感受。浪费一个上午最重要的是,不应该感到内疚。

If you find yourself fretting about “shoulds” or “to-dos,” find the nearest cafe and order another coffee and stay exactly where you are. You are not being lazy, you are investing in the leisure. In the mental space where new ideas happen to wander in, wearing a linen shirt and asking if this seat is taken.

你如果你发现自己为应做的和待办的烦躁不安,找到最近的咖啡馆,再点一杯咖啡,待在那里不要动。你不懒,你是在为休闲投资。在新想法发生的心智空间内,穿着亚麻衬衫,询问这个座位有没有人坐。

在这个早晨,如果在十分钟以内做好了咖啡,那都是过错。效率绝对不应该考虑。

You might be reading, again. Maybe you’re watching someone water a balcony garden, or listening to a conversation in a language you barely understand. Maybe you’re simply enjoying the pleasant fact that no one needs you for anything right now.

你可能又开始阅读了。或许你在看某人给阳台的花园浇水,或者听着你几乎不理解的语言的对话。或许你只是在享受现在没有人需要你做任何事情的愉悦事实。

这样就够了,不要试着说什么有趣的话、做什么有意义的事情,去感受生活本身,什么都不要做。

You’ve accomplished exactly what was required of you.
你精准地完成了你需要做的事情。

Absolutely nothing.
什么都不做。

And you did it well.
而且你做得很好。

Until next time,
直到下一次……

如何嬉皮笑脸地应对「为你好」的人

📻

树老师认为她能保持心力的很大原因是,她面对打着「为你好」旗号来管她的人能够做到不上心。这种人无处不在,甚至与权力结构和社会地位无关,不只有上位者管理下位者,平辈和下位者也可能以各种理由管别人——这是一种人格缺陷。跟这些具有人格缺陷的人讲道理是没用的,在某些极端案例中,这些人根本不讲道理,仅仅是想在你的生活里找点存在感。树老师说她奶奶会给她和她的表妹完全相反的人生建议,仅仅是为了「管管他们」。某种程度上,这些人是在给不存在的公众表演,这种公众在落后的社会中可能存在,但在现代的、原子化的社会中,表演美德没有意义。很多人逃不掉这种思想钢印,总是在表演,「管管你」就是一种表演,对长辈来说,这是尽到教育责任的表演(尽管他们根本不懂教育,仅仅是自以为是而已);对另外的人来说,这也是某种把自己塑造成热心的好人的表演。

我想补充的观点是,这种人格缺陷有大有小,也可能以不那么明显的症状出现在年轻人身上。有较轻「症状」的人可能也会抱怨「为你好」的管教,而意识不到自己也在做一样的事情。仔细想想的话,那些试图把「正确方式」教给你的人无处不在,比如使用某个编辑器的正确方式、查阅资料的正确方式、做笔记的正确方式、软件架构的正确实践等等。

这些建议往往都忽略了特异性,人与人之间特质和需求的差异,不同情况下对解决方案的不同要求,而且就算他是对的,有些东西也需要别人亲自试过之后才能得出结论。可笑的是,如果反对他们的意见,他们可能会觉得是你「不懂做笔记」「没有把笔记当作笔记来对待」。他们为什么跨越社交边界去管这些琐碎的小事?我认为和「为你好」的长辈一样,都是人格缺陷,只是表现和轻重有别。

这些观点当然是有意义的,对个体和一部分群体来说可能很好,但硬塞给别人无异于精神强奸。甚者,可能发展成邪教:这个东西这么好!我们都这样用!你为什么不用?!你必须要这样用,因为…… 这些东西写在你自己的博客里就行了。

我最近的确被太多这样的人困扰了,一方面是来自学校,关于老师的各种可笑行径和言论我已经在联邦宇宙上写了太多了,再写只会消磨自己的心力,这里就不再赘述了;另外还有来自现实及网络上各种琐碎的社交,某些强硬的观点也让我喘不过气,甚至让我对原本很感兴趣的事物都产生了厌恶情绪。我不清楚这是不是我的人格缺陷,我对所有事情都很容易上心。我在《 这个世界会累死完美主义者吗? 》里讨论过这一特质给我带来的困扰。我一直想要快点毕业,找到工作,去到我自己选择的城市,远离我讨厌的人(一两年前,我甚至想要 远离这个国家 )。

不过就像树老师在播客里说的,无论逃到哪里都无法远离这种人,他们无处不在

我在阅读以前的文章时,发现自己当下的困扰与一两年前的困扰惊人地相似:

我要是这么写下去,你大概会觉得可笑吧,看着这个本科生像小孩子一样抱怨自己的老师…… 我也觉得自己可笑,明明把自己的生活安排得好好的,只因为台上那个自我感觉良好的男人能对自己指指点点,心情就一落千丈。

但我实在难以接受,自己每周要被他浪费 270 分钟的宝贵时间,听他用 20 分钟解释我 5 分钟能理解的概念,然后听他时不时爹味爆发,教我们要怎样过好自己的生活,还发表一下自己对于哪些人应该拿毕业证、哪些人就不该发毕业证的看法,全是不着边际的空谈,没有任何实在的方法和新颖独到的思想。我每次听他讲课,心里都在呐喊:你能不能不要再浪费我的时间了!

——《 Fly Me to The Moon

我一直,都无法停下在乎蠢货说的话,我恨,那些蛮不讲理、学识浅薄、过度自信的人,能肆无忌惮地浪费我的时间和摆弄我的自由,而我什么也做不了。我一直都学不会屏蔽他们的话,因为,如果我作为人类,和自己存在的环境完全解离,那会是多么可怕的事情!而且,我向来讨厌说谎,我不喜欢说违心的话,只为了让那些蠢货能离我远点。

我的策略一直是主动远离,远离那些完全无法沟通或者我不愿意费心费力沟通的人,但我想我该开始接受身不由己的事实,在公众面前、在老登面前,应该撒谎、应该表演、应该解离,以便获得内心的安宁。

如何让小孩愿意跟你讲话?

📻

以「专家」口吻反过来教育不懂事父母的播客。小孩(这里指二三十岁,不再依赖原生家庭的成年人)不愿意跟父母讲话并不是他们变了,而很有可能是因为他们一开始就不愿意跟父母讲话,但是以前没有自主能力,必须吃父母的、用父母的,寄人篱下,所以必须跟父母讲话。等孩子长大了,翅膀硬了,获得了可以不跟父母讲话的特权,自然就不会讲了。

「专家」提出了两种解决方案:

  1. 给钱。小孩肯定愿意跟老板说话,点头哈腰地提供情绪价值,那父母继续给小孩砸钱,当他们的老板,小孩就愿意跟父母讲话。如果还是不愿意讲,那就是给得不够多。
  2. 风水轮流转,现在,轮到老人被小孩管教了。小孩不能成为问题小孩,父母如今也不能成为问题老人,倔着要在小孩的家里按照自己的方式生活、要求别人按照自己的方式做事、不接受任何新事物,就是问题老人。要让小孩跟自己讲话,就不要做问题老人。

我很喜欢这期博客里的「专家」口吻。我希望某天我不会需要把这期博客发给我爸听…… 就算需要的话,我可能也跑得远远的了。


茶歇

NPM 的安全更新

📃

NPM 即将在七月份发布 v12 版本,主要有三项改动,均涉及 npm install 命令:

  1. 默认禁用脚本,安装前、中、后,依赖都不能执行脚本,除非手动允许
  2. 默认禁止从 Git 仓库解析依赖,无论是直接还是间接的依赖,除非手动允许
  3. 默认禁止从远程 URL 解析依赖,除非手动允许

更多技术细节可以阅读 原文 。安装前后执行的脚本、从 Git 仓库和远程 URL 下载的外部代码,都是很多 NPM 供应链攻击的来源。之前我在 第 83 期周刊 分享了如何用 PNPM 规避供应链攻击,其中提到的策略也几乎一致。

不过这仅仅是客户端层面的防控措施,为什么 NPM 不能想办法加强中心包索引的安全性呢?哦等等…… 我在 Hacker News 的讨论区发现了什么?—— NPM 在 2020 年加入了 GitHub ?原来也是微软的一部分,那不稀奇了。

我怎么感觉 JavaScript 生态不会好起来了?

Homebrew 6.0.0 发布

🍺

macOS 事实意义上的默认包管理器发布大版本更新,主要变化有两个,一是新增了 Tap 的信任策略,在未受信任之前,Tap 里的代码无法运行,避免执行恶意程序。看起来大家都终于开始在意软件包安全性了。

另一个变化是 Homebrew 的 API,现在 brew update 运行更快了,因为 API 变小了,Homebrew 需要建立的网络连接也变少了。Homebrew 的性能在此之前一直都是个问题,我在 第 74 期周刊 推荐过 nanobrew ,是用 Zig 写的第三方 Homebrew 客户端,据说速度快很多,不过我的实际体验是,速度并没有很快,有些时候甚至更慢,而且由于刚推出没多久,Bug 也不少。新的 Homebrew 体验上确实快了不少,不过本地的网络环境应该还是最大的瓶颈。

顺带一提,我其实一直没有下定决心折腾 Nix,我感觉它太重了,以及我也没有那么在乎「可复现性」。对我来说,只要管理好关键软件的 dotfiles 就好了,比方说 Neovim 配置和 Ghostty 配置等等。有不少我用的软件包没有 Homebrew Formulae,如果要在集中管理和轻量化之间取折中方案,我想可以自己维护一个私有的 Tap,写一些 Formulae 自己用。不过这就意味着我要学 Ruby 了。

Chromium 或将无法使用 uBlock Origin

📃

谷歌即将终止对 Manifest V2 的支持,广告、追踪器拦截插件 uBlock Origin 就依赖 MV2 提供的 API,一旦 MV2 不受支持,在 Chromium 上就只能使用基于 Manifest V3 的 uBlock Origin Lite(并不好用)。Microsoft Edge 很早就禁用了 uBO,Opera 表示会跟进,目前只有 Brave 表示会继续支持 uBO。

我一直在使用 Helium 浏览器,它也是基于 Chromium 的(准确来说是 ungoogled-chromium ),所以我很关心去谷歌化的 Chromium 系浏览器这边状况如何。不久前,ungoogled-chromium 的 Issues 区有 相关的讨论 ,维护者表示他们还没有升级到 Chrome 151(也就是 MV2 被彻底禁用的版本)。之后的讨论…… 看起来很激烈,其中一位贡献者的 长篇大论 中甚至提及了要给浏览器添加在 Chrome Web Store 以外的地方安装浏览器拓展的能力。还有很多讨论是关于 uBO Lite 究竟好不好用的,ungoogled-chromium 如何继续支持 MV2 这个问题,我没有找到答案。

Helium 这边,从 README 来看,尽管基于 ungoogled-chromium,他们仍然做了很多修改。在发现了一条过于幼稚导致我厌人症又发作的相关 Issue 之后,我终于找到了主要维护者 wukko 的答复——一位用户请求 Helium 从 Supermium 合并补丁,以此持续 MV2 支持,wukko 的回复是:Helium 已经支持 MV2 并且自带 uBO 作为浏览器组件。看起来 Helium 用户不必担心 uBO 问题。

至于为什么不用 Firefox,可以阅读 第 18 期周刊 (不过那是一年前的消息了,如今可能有变化,但整体而言不太信任 Mozilla)。如果非要用,可以试试 Waterfox 或者 LibreWolf 。我不愿意使用 Firefox 系浏览器的主要原因是他对某些新 Web API 和 CSS 特性的支持实在太慢了——比方说,截止 2026 年 6 月 10 日,主流浏览器中只有 Firefox 没支持 View Transition

为 YAML 辩护

📜

文章简单介绍了配置文件格式的历史,从一开始不规范且扁平的 INI 文件,再到能够表达层级但过于臃肿的 XML,之后是适合数据交换但对人类编辑不够友好的 JSON,最终是这篇文章的主角——YAML 和 TOML。

顺带一提,我永远不会错过批评 Java 的机会:

Anyone who maintained a Java web.xml or an Ant build file in 2003 knows what it was like to edit dozens of nested elements just to change a database connection string. The verbosity made the files difficult to maintain by hand, which is precisely what configuration files demand.

任何在 2003 年维护过 Java web.xml 或者 Ant 构建文件的人都知道,编辑一大堆嵌套的元素,只为了修改数据库连接字符串是什么样的。文件冗长使其很难徒手维护,而这恰恰是配置文件需要的。

如今 Spring Boot 简化了很多配置,但 Java 生态依旧使用 XML 格式,还有另一个令人闻风丧胆的 properties 文件,可以说是完美融合了 YAML 和 TOML 两边的缺点

关于 YAML 和 TOML,主流的论调是:TOML 比 YAML 更好,而喜欢用 YAML 的人大多都是不了解情况(uninformed)。YAML 的语法难以预测,比方说 no 可能是挪威的国家代码,但会被 YAML 解析为 false 布尔值,offNO 等其他字符串也是。问题在于 YAML 的隐式类型转换,有无 "" 都会被视作字符串,大家都习惯不打引号了,而 TOML 必须显式地使用引号才会被解析为字符串。

此外 TOML 还致力于成为标准化的 INI,两者的语法类似,TOML 胜在能用方括号表示层级。尽管语法符合直觉,解析器也很容易实现,但对于深层次的数据结构,TOML 的表现力不如 YAML。如果嵌套较多、列表结构较多,TOML 就会变得很难读。TOML 也因此遭到了批评:

TOML is a bad file format. It looks good at first glance, and for really really trivial things it is probably good. But once I started using it and the configuration schema became more complex, I found the syntax ugly and hard to read.

TOML 是糟糕的文件格式。刚开始看起来很好,对非常、非常简单的事情或许也很好。但一旦我开始使用,并且配置格式变得更复杂之后,我发现语法变得很丑并且很难阅读。

—— Martin Vejnár

作者在文章最后指出,YAML 1.2 规范解决了以挪威代码为代表的问题,noyes 等词不会被解析为布尔值了,只有 truefalse 才会。六十进位数被移除了(所以 22:22 等端口配置不会出错),时间戳不再是核心类型(2026-05-05 会被解析为字符串,而不是自动解析的日期),JSON 是 YAML 的子集了,任何 JSON 文档都可以被 YAML 解析器解析(尽管我想不到这有什么用)。人们认知中问题百出的 YAML 是 1.1 版本,而 1.2 版本已经没有这个问题了。

不过我还是觉得,字符串应该被 "" 包裹,不应该依赖隐式转换。

支持 YAML 1.2 的解析器较少,所以作者介绍了 py-yaml12 ,这是用 Rust 写的 Python 库……?

据说是为了与 PyYAML 竞争,这个库是用 C 写的(也有 Python 的实现作为回落方案)。在性能上,由于 py-yaml12 使用编译好的 Rust 程序而不需要解释 Python 程序,性能可以与 PyYAML 竞争,同时它还支持 YAML 1.2,也是目前很难找到替代品的优势。

我的态度是,简单(尤其是层次较浅)的配置文件用 TOML,如果有复杂的配置要求,可以考虑 YAML 1.2。不过,如果配置文件真的足够复杂,用 JavaScript 或者 Lua 等脚本语言作为配置语言,貌似也不错?


星群

clj.rs

用 Rust 实现的 Clojure 方言。我之前还试过用 Go 写的 Glojure ,不过工具链很不成熟,如果不是在 Go 语言里嵌入简短的 Clojure 代码或者运行几个文件组成的小项目,Glojure 的体验较差。

我还没有试过 clj.rs,但目前处于 Alpha 阶段,稳定性和成熟度可见一斑。我对 Rust 也不熟悉,暂时不会尝试,没办法做贡献了。不过,看到有这么多人想要把这门语言从 Java 生态里出来,还是很高兴的。

访问: clj.rs


切片

  • 六月九日
    • 对人类感到厌倦,不想和任何人讨论任何事情,一鼓作气,在 uBlock Origin 里拦截了我的联邦宇宙实例和 Matrix 服务器,先让自己清静一段时间再说。以后有什么要发的,就先写在周刊里好了。

    • 看了《Rick and Morty》第九季第三集…… 编剧你写的什么玩意儿?——令人意外,但这并不是批评,我也不知道这是什么。

    • 七八点的时候突然想吃点东西,看到有卖墨西哥卷饼(大概就是传说中的 Taco 吧)的店,挑选了半天才发现单点不送,再加了一份薯条也不行,必须要我点两个饼。这是什么道理?!

      故点了一份披萨,从热量来说都不相上下呢。

    • 觉得 LLM 生成的内容很有道理并从未想过反驳,是否可以理解为「此人的认知水平在人类平均水准以下」?

  • 六月十日
    • 结束糟糕的一天之后的发泄方式:在饥荒里带着一只熊獾把一片森林砍到只剩光秃秃的一小半。

      堂堂秋季 Boss,从来没有被我亲自拿下过,每年都把它拉到森林里好一阵狂砍,接下来就是树精守卫和蜘蛛女王的事情了…… 月后等他变异成装甲熊獾之后还能看他反杀,再捡一地的活木和蜘蛛丝。

  • 六月十一日
    • 在 Go 语言里写 if err != nil 的时候我总是用法语把 err 读成 /eʀ/1 而不是英语的 error直觉上觉得 err 没有第二个元音,但有第二个字母 r,不能读成 er 也不能读成 error

      于是就自然地读成法语了。就当作练习小舌音吧。

    • Hell yeah. Screw you, life, you watch me live the crap out of you.

  • 六月十二日
    • 不用流媒体,所以不能第一时间听到 Olivia 的新专辑。发现她官网有售卖数字专辑,十美元,结果只能下载 MP3 格式,而且只开放给美国顾客下载…… 我除了 Piracy 还有别的选择吗?

    • 给社交媒体和 Matrix 屏蔽了之后,我居然十分自然地跑到了 Backrooms 的 Discord 服务器聊天吹水…… 果然摸鱼的需求是恒定不变的吗?

    • 我也是女性主义者,但我必须承认跟很多同样主张性别平权的人讨论一些严肃话题时非常累。我仅仅是指出,在文学作品中,男性和女性有差别,他们就下意识地以为我是要在文学作品中鼓励「性别刻板印象」,把我当作了敌人,开始尖锐地要求我解释我说的话。

      实际上我的观点是,社会构建的性别之间有典型性的差异,比方说在如今的社会环境下,女性会因为性别遇到很多问题,然而这些问题是男性不会遇到的。这种差异当然是错的,所以需要被讨论、被看见,在文学作品中展现人文关怀,去写那些具体的人会经历的真实遭遇。

      结果我得到的答复是:我不同意,男性也会遇到问题。当然!我的观点从来不是男性的问题比女性少,我说的是女性会遇到男性不会遇到的问题,而当然,另一半没有说完的话是,男性也会遇到女性不会遇到的问题,而这正好印证了我的观点——男性和女性在社会构建的性别下有差异,并且这种差异具有典型性,是值得分开讨论和关怀的。

      而对方的答复是:这是人们为了维护自身利益所做的行为,和染色体无关。我真的累了,我觉得许多人没有生理性别和社会性别的概念区分,染色体显然是生理性别(即「性」),而我一直在谈的是社会性别(或者说「社会身份」)。我从来没说这些期许和规训是正确的,我只是说这些具有典型性的差异和具体的人所遭遇的事情,需要被讨论,在文学层面是有差异的。

      我们一开始的分歧点是:对方认为所有性向的感情写起来都一样,我不同意,指出性别本身就会导致不同的个人经历,不同性向所面临的社会问题也有所不同(比方说女同就很少有「恐艾」情绪),就像前面所说的性别问题一样,有值得分开讨论的典型性。

      最后我才搞明白,对方的论点是「重点是写爱,爱是一样的」,可我一开始说的就是「如果只是纯粹写感情而不隐射现实,不关心现实中具体的人,或者写在完全架空的世界观(没有已经建构的社会性别)下,那所有性向的感情的确区别不大(忽略个体性的话)」。实际上我们的观点是相似的,结果吵了这么久!我认同(忽略个体性的话),爱的确没有区别,只不过我认为写爱情也要写周围的人和环境,不能完全剥离。

      或许我最大的问题是:不该在即时聊天群组里试图和人进行严肃讨论,哪怕是质量较高的小社群。

    • 我越来越不知道怎么面对人类了,或许只有在所有人面前嬉皮笑脸才能不消磨心力。如果找不到可以无阻碍表达观点的朋友,我想我只能寄希望于以后养的狗狗可以安静地坐下来听我跟他讲话了。

      一页漫画,一只粉色的兔子说“Anon, you have Autism, so don’t try to understand normal people, cuz you never will”

      你有自闭症,不要尝试理解正常人,你理解不了的( 来源

  • 六月十四日
    • 在去 过生日 的路上,刚上地铁就收到了 Tagtax 的赞赏,竟然记得我的生日吗?真是惊喜。

      如果读者不知道的话,有个叫作 Tagtax 的读者每个月都会给我打赏,真的给了我很大动力。如果你在读这段文字的话,谢谢你的支持!

    • 之后还收到了 Doin 的赞赏,谢谢!


  1. 这是 国际音标 ,不要和国内常用的 DJ 音标(一直被误以为是国际音标,实则不然)搞混了。其中 ʀ 是小舌颤音。 ↩︎

一个人的生日

2026年6月14日 23:20

写完这个标题我感到奇怪:除非是多胞胎,生日不就是一个人的吗?当然,我的意思是今年我过生日没有人陪,但我仍然过得很开心,甚至是我最开心的生日之一。

十八岁生日,那是在高考结束的几天后,我和我的高中同学一起过。我们去玩了剧本杀,之后一起到我家吃了火锅,之后便聚在一起聊天。我在高中一直是公开性取向的,有个女同学甚至让她的男朋友给我送了一大束玫瑰花——现在想来,那好像是唯一一次有男人给我送花。不过没关系,我会给自己买花的。当时的桌子上聚了十个人左右,其实还发生了一些小矛盾,毕竟来的不全是亲密无间的朋友,大部分人只是每天都见面,相对而言走得比较近而已。

十九岁生日,是和我在大学认识的朋友一起(没有一个是和我同一个学校、同一个班级的,我现在仍然和同班同学走得很远)。我不记得我们干了些什么,我只记得最后我们五六个人在私人影院看恐怖片。那是在我和 M 表过白之后的事情,我们决定做朋友,但我不能否认我对他还有情感(甚至现在也是)。我记得那天他先走了,因为他的学校有宵禁。之后他跟我发消息,说他在跑回寝室的路上摔了一跤,我们之后又聊了聊。和其他人发生的其他事情,我都忘了。

二十岁生日,是在我刚从寝室搬出去不久后。人越来越少,除了如今我已经很厌烦的同室(当时还没有合租,所以有恰当的距离感),还有另一个我一不小心喜欢过的直男(我讨厌我自己……)。另外两位是关系不错的女性朋友,但当时已经疏远了,是被我生拉硬拽出来的。当时只是在海底捞吃了个饭,去 KTV 唱着不尽相同的歌——说起来,我已经忘记被海底捞员工围着唱歌有多尴尬了。

今年六月,到了二十一岁生日。前一天,也就是六月十三日,我才终于从找实习、做笔试、做专业知识的查漏补缺和各种琐事中回过神来:等等,明天好像是我生日来着?其实上周日在做周计划时我就意识到了这一点,总觉得时间到了,计划就会自然而然地发生,可到了周六我才反应过来。我是不是该叫人出来聚聚?

叫谁呢?同室是绝对不会考虑的。我从通讯录里翻找那些发出邀请也不会觉得尴尬的人,被排除的那些人,要么是太久没联系,加上本身也没那么要好,觉得不方便联系,要么是我知道他们很忙,不会有空来的(我的交际圈里有相当一部分是在教培机构上班的,他们基本上周一才休息)。六月真是不适合聚会呢,大家都在忙结课作业,准备期末考试,到了这个年纪,也开始准备考研,或者像我一样准备找工作了。看着陆陆续续有人发出的毕业照片,这个时候我的生日貌似也显得无足轻重了呢。

问了几个朋友,都没有方便的时间。在等回复的期间,我坐立难安,我已经被现代社会训练成了无法面对不确定性的样子,毕竟外卖软件还能看到外卖员的实时位置呢。等到下午终于确定没有人能来陪我过生日的时候,我很平静地接受了事实。毕竟,除了对亲密关系的渴望,我从来没觉得孤单一人是痛苦难忍的(对前者的需求,我想可以通过养狗来解决)。

此时,有个 P 人一怒之下(并没有),做了第二天的出行计划。

大概就是搜了一下附近比较适合一个人的活动,发现有一所美术学院正好在毕业季的时候办集市,有各种手作的贩卖摊位。尽管没有在帖子里看到比较感兴趣的商品,但过去逛逛也不错。然后很自然地又找到了观音桥附近的几家手帐店,正好最近在淘宝上一直发现不了喜欢的手帐素材,说不定可以在线下挑到(实际上也没有)。还在意外之中发现了一家口碑很好的咖啡店(到了之后我才发现我要去逛的书店、手帐文创点,都和这家咖啡店挨在一起啊)。看到手帐圈里很多人用 MUJI 的文具,素素的而且很耐用,所以也加入了行程。不过你们怎么都正好在观音桥啊?未免有点太方便了吧。我感觉每次出门玩,目的地十有八九都在观音桥。

详细的图文我基本都发在朋友圈了,和博客的读者,还是保留些许距离感吧。不过,朋友圈或许只适合展示那些最浅显的「我做了什么」「看了什么」「买了什么」的部分,在博客里,就让我用文字写写我的感受吧。

我的感受就是,现在我的脚和钱包都痛得要命,但是心情是非常愉悦啊。

去集市逛了一圈之后,我只购入了一张版画画作的明信片,和两个画有猫猫图案的杯垫——说真的,把我家的杯子和杯垫放一起,都有好几个杯垫没有杯子放。相比常见于手作集市的明信片和冰箱贴,杯垫对我来说算很有用的了。路上还遇到了现场给宠物画像的摊位,可以把自家猫猫狗狗的照片拿去,画在勋章上(或许更常见的说法是吧唧,也就是 Badge 的音译词)。这种时候就很想养宠物。

由于人实在是太多了,还有很多带着小孩子进来的家长,吵吵的,也差不多到了中午,我就离开了,坐上了地铁。由于这几天吃得太多(和家人视频通话的时候,我爷爷说我变「健康」了,这个时候我就知道自己压力太大,暴饮暴食的结果显现了……),前一天也是把肚子填埋了才上的床,所以我就没吃午饭(和早饭),反正晚上会吃一顿好的。

北城天街的 MUJI 还挺大的,有三层。我很喜欢在 MUJI 里走走看看的感觉,尽管我什么都没买,但它给我一种朴素且美好的生活的愿景。店里没有什么花里胡哨的装饰,都是一些质量尚可且实用意义很高的商品,衣服也都是可以没有压力购入的基础款。我的目的主要是文具,可惜这家店的文具区域太小了,也没有太多好用的本子。至于收纳盒,这东西我早就不买了。所以,尽管大部分东西我都很喜欢(他们一二层还有冰淇淋欸!),我还是空手走出了 MUJI。兴许离开大学,搬家之后,会到这里来找我想要的生活。

接下来我要去的是小悟循环书店(很巧,和 Totomato 咖啡店以及一家我要去的手帐店挨在一起),开在老居民楼的门口,外面还有老人在下棋打牌。书店很小也很安静,店主在很多书上都贴了便签,写了推荐语,字很好看。我刚扫了一两排,就看到了加缪。大部分我都看过了,买了一本没有的《第一个人》,还挑了几张写有加缪摘抄的明信片。书店外围原本是阳台的位置放的都是手账本、贴纸和便签。A5Slim 用户是没办法在这种店里找到心仪的内页和素材的,只有 TN、A6Per、A7 和 Μ5,接受现实后含泪购入贴纸离去。贴纸是拿铁盒装的,貌似是店主的原创设计,上面有凹凸不平的纹理,色调也很喜欢。

一栋老居民楼,但有一部分刷成了白色的墙,这时一间小书店,透过窗户能看到满书架的书和各种文创,店外摆着「折扣书店」的牌子,还有两个红色长椅。

书店的门面

摆满了书的桌面,一些书本上贴着便签,店主在上面写了推荐词,字体灵动但整洁。

店主给很多书都写了推荐词

桌面上摆着加缪的《第一个人》,两张明信片上面的句子分别是「愿你活得激烈,愿你活得充沛,愿你活得如同没有明天」和「自由不是舒适的选择,而是艰难的责任。它意味着你必须清醒地面对世界的荒诞,并依然选择前行」,还有一个画着红色摩卡壶的贴纸。

我买下的书、明信片和铁盒装的贴纸

付款后店主用牛皮纸袋装好了书,还盖上了印章,用贴纸封了口,看起来很像用牛皮纸扎得紧紧的包裹。我想起了《音乐之声》里 My Favourite Things 的歌词:Brown paper packages tied up with strings…… 抓在手里的时候会有让人觉得很舒服的响声。

书店的氛围我很喜欢,开在老居民区里想必租金也不贵,要是能把自己喜欢的书放在一起、写点推荐语,再做些文创和贴纸,夹带私货力荐喜欢的作家,到店里来的客人都很安静、有礼貌,这应该是很棒的工作吧。要是店铺大些,还能冲冲咖啡,如果有闲心的话,学学烘豆子。选书和做咖啡两头一起抓,不知道能否作为竞争优势。当然,开店背后一定也有很多糟心事,我现在也不知道自己要去往哪个城市,这个决定还是以后再做吧。

之后就去隔壁一家名叫快乐上映的文创店看了看,店里有一半都是重庆旅游的文创,对我这个本地人没有任何吸引力;另外的一半大部分是我从来不用的和纸胶带(高一时买过一大盒,用到大三还没用完,最近断舍离丢掉了)。我其实很想买标准尺寸的 TN 本,因为我会用来做拼贴,也会夹在 A5Slim 活页的前后做日计划和阅读笔记(A5Slim 的尺寸和 TN 标准款的尺寸一模一样),不过,我的审美偏好真的很难和大部分的手帐爱好者对得上。快乐上映在四分钟步行路程的地方还有另一家分店,选品丰富一些,但都类似,没有激起购买欲。

再隔壁就是那家好评连连的咖啡店,进门之后店员打招呼很热情。他们的拼配豆名字起得很有特色,深烘拼配叫作黑猫,中深烘拼配是花猫,我点的是浅烘的白猫美式。白猫豆的风味描述是莓果、芭乐、柑橘、洋甘菊、绿茶和焦糖,除了绿茶不太明显,而我只吃过一次芭乐早就忘记味道之外,我的感受和描述基本符合。尤其是莓果,酸甜调很浓烈,降温之后完全是甜水,很好喝。

由于咖啡太好喝,一大杯美式的量也很足,我也就多坐了一会儿。旁边全是人在聊天,因为地方小,所以比一般的咖啡店要吵一些,但其实还能接受,毕竟我心情很好。戴上耳机之后,我读了一章《工作、消费主义和新穷人》,第四章的内容大概是将工作伦理在消费者社会是如何把几类不相干的群体贬为底层阶级,又如何规避贫穷所带来的社会责任的。读完这一章之后,我的手机和手表电量也充得差不多了,便收拾好东西,去前台买了一包白猫拼配。花了一百块,所以克价大概是四到五毛左右,还送了一个洗碗擦和一些贴纸,很满意的消费。

出门的时候店员跟我说了一声「谢谢你」,我想除了一百多块钱的消费之外,在喝完之后购买他们烘焙的豆子的确是很高的认可吧。毕竟我点的可不是特调或者奶咖,美式可是能最纯粹、最精准地喝出一支豆子的好坏的(除非是杯测,但没有店里会卖这个吧)。我没有意式机也没有摩卡壶,我在想我该怎么萃这支豆子比较好,或许可以用法压壶或者爱乐压。

桌上摆着一杯浅杏色的咖啡杯,杯子圆润粗大,杯子里的咖啡上浮着一些白色的泡沫。杯子下压着小票,小票上或者三个跳芭蕾舞的大叔(是的,是大叔,长胡子和秃头的那种)。豆卡上写着「白猫;好猫系列拼配豆;风味描述:莓果、芭乐、柑橘、洋甘菊、绿茶和焦糖」

白猫美式、收据和豆卡

购物袋上画着一只穿围裙的哥斯拉怪物,它一只手拿着奶泡缸,另一只手拿着咖啡杯,嘴里吐出激光击碎了杯子。下面写着一行字「I love coffee so I’m incredibly strong」

买了一包豆子,这是购物袋

豆袋上画着一个抱着巨大咖啡豆跑步的胖胖大叔,上面写着「拼配咖啡豆」「白猫 Blend」「227g」;旁边摆着一个人形洗碗擦,上面的男人带着蓝色头巾、穿着蓝色大褂和天蓝色的宽松裤子;还有一些各式各样的贴纸。

豆子,还有附赠的洗碗擦和贴纸

再往上走是塔坪,还有各种开在老居民楼下的小店,我要去的那几家基本都开在那里。下一个目的地是名叫 Boom Cup 的杯子店,貌似也挺火,不过我没有找到什么心仪的杯子。或许我对杯子的审美和我对纸品的审美趋同,和大众喜欢的相差甚远吧。离开之后我有些迷路,便在旁边随便转了转,发现了一些小店,门面都挺好看,还有一家文创店门口全是绿叶。绕了好久才找到下一个目的地,也是我此行最满意的一家手帐用品店,名字叫作夏眠商店。

店里大致可以分为三个区域,但都是很小的房间,外面的两个是商品区,里面是收银区。店长坐在最里面,收银台做得很高,收银时也不怎么说话,倒是很有安全感,对她自己和顾客来说都是这样。进门的区域主要是和纸胶带、贴纸、便签和本子。我又看了好久 TN 本的货架,没有找到喜欢的,失望。便签里倒是有一些不错的,色彩大概可以用茶、土壤和泛黄的照片来形容,比那些亮眼的、画着卡通角色的纸品要更吸引我。里面的房间摆了一桌子的印章,可惜我没有印泥,不然就买了——好啦,其实也是因为没有找到喜欢的图案。

走着走着突然很想吃寿喜锅,于是打开美团搜索,找了最近的一家店。路上偶遇一家苹果直营店,进去摸了摸 MacBook Neo——我还是想不明白谁会买这个产品,它甚至没有 Air 轻薄啊。结账之后才发现,生日当天就餐可以优惠…… 70 元,意思是我原本可以只花 60 块钱?罢了罢了,吃了很多肥牛片、哈根达斯和娃娃菜(可恶啊,那么多素菜,娃娃菜竟然是第一个被拿光的!),也算满意了。这一天过得很不错,我出门就把多花了钱这件事情抛到脑后了。仔细想想请人吃饭的那几年生日,给自己多花七十块钱算什么。

吃寿喜锅之前还路过一家 X11,本来对这种塑料小玩意儿贩卖店嗤之以鼻,结果隔着玻璃看到了伊布。走近一看发现是宝可梦的盲盒,呵,盲盒这种东西,在打开之前都不知道自己在为什么付钱,为什么有人会买这种东西?竟然还要八十八块钱,里面有三海地鼠、杰尼龟、具甲武者…… 嗯?我手上是什么?我的钱呢?!

桌上摆着一个宝可梦手办,角色是海豚侠,形象是一只站立的海豚,身上有波浪样式的护具,像超人一样叉着腰;旁边是一叠宝可梦卡牌。
还是海豚侠,但是宝可梦卡牌都被翻开了,整齐地摆在一起。

噢我的天呐,是海豚侠!还有宝可梦卡牌!虽然这么说不太好,但我真的害怕我开到的是大舌贝。

提着四个购物袋就餐,再坐地铁回家,倒是没有觉得羞耻。毕竟这是我的生日啊!回到家清点战利品的时候也很开心。我最喜欢的应该是这个杯垫了,简直是一流的喝水体验。

从上往下拍的透明玻璃水杯,透过水能看到下面的杯垫,杯垫是一只眯着眼睛看起来很生气的三花猫,表情就像是被杯子压住了很不爽一样。

“人类,谁允许你这么压着我的!”

如果有一两个亲密的挚友一起度过,这一天会变得更好吗?或许吧。或许牵着一只萨摩耶出门也会变得更快乐。但这些我现在都没有,那就享受暂时还没有失去的自由吧。

稻草人周刊 Vol.84

2026年6月8日 07:43

这周更新还挺勤的,于是周刊的内容就变少了,合情合理。


止语

Hit the Wall music cover

Hit the Wall

Gracie Abrams

Sooner or later, you’ll find out

I live in a pattern of breakdowns

You’ll bend to my silence, it’s so loud

And then you’ll lose me to the crowd


连接

分析正确与否,问题依旧存在

📜

作者观察到,当有人指出问题时,其他人可能会试图解释问题背后的原因,然后,人们就理所应当地觉得问题被合理化了。可是,无论原因是什么,问题依旧是问题。坏的问题不会因为一开始的那个人的分析错了就不坏了。比方说,有人读到一篇文章的标题和文章内容毫无关联,此时有人解释「你知道标题很多时候不是作者写的吗?是他们的编辑写的标题」,然而无论标题是谁写的,标题都和原文无关啊(而且作者表示,他向作者指出标题问题,往往都会得到回应,他们是能和编辑沟通的)。

问题不会因为正确的分析就变得合理,不过人们好像理所当然地觉得,问题若是有了清晰的原因,搞出问题的人就没有责任,不该被责备,也不需要改变了。

另一个层次的例子是,当有人指出问题,负责的人解释了问题的缘由,而提出者却说:“噢,那也不是借口啊。”事实是,负责人从来没说过要用原因为自己开脱,更有利于解决问题的讨论方式是:我们了解问题发生的原因了,接下来要怎么改进和预防?

文档写给谁看?

📜

The Rust community doesn’t want beginners to learn Rust — it wants advanced programmers to learn Rust, which could even be fatal for the language in the long run.

Rust 社区根本不想让初学者学 Rust——他们想让高级程序员学 Rust。长期而言,对这门语言来说是致命的。

这不是长文,仅仅是作者近期事务繁忙、决定让自己慢下来的日志。他想要慢下来的其中一个原因是他在学 Rust,而这门语言的 文档 写得非常不利于入门。尽管我读 Rust 文档的时候(是的,我还试图学过这门语言……)不觉得特别难懂,但如果有更多详细的解释和例子当然是更好的。

我想之前读过另一篇博客文章(我找不到链接了),作者想写个简单的 Pastebin1 应用,顺带学一门新语言。他一开始选择了 Zig,却发现找不到方便理解的文档和教程,Zig 有 HTTP 相关的标准库,但他并不知道如何使用。这也和这门语言迭代太快有关系吧。之后他换成了 Go,发现了不少好用的教程,顺利完成了项目。

Go 语言各方面的工程思想都深得我心。Rob Pike 本人就说过「 Documentation is for users 」(文档为用户写),写文档不应该写「这个函数做什么」,而应该写「这个函数用来做什么」。那场演讲里还有一句话我印象深刻:

Don’t be afraid to explain things so that it makes sense.

不要害怕把事情解释清楚。

我近期还有一些个人的经历与这个话题相关。前几天项目验收,讲完之后老师(她是为数不多我比较尊敬的老师)问了我的职业规划,也鼓励我可以往运维支持和产品经理这两个方向走走看看,因为她觉得我的综合能力还不错。她说很多同学在讲项目时总是在讲细节,比如项目用的是什么数据库,某个单独的功能模块里能做的所有事情,很难让他建立起整体的印象。

其实我觉得自己讲得没有很顺,但的确,我是站在「老师作为评价者需要听到什么信息」的角度来讲述的,我先讲了最精炼的业务需求,再讲作为课程项目,这个系统的重难点是什么,然后再逐个介绍功能模块。这之后她几乎没有关于软件系统的问题问我(也有可能是她对项目本身不感兴趣吧)。

我想就和做任何事情一样,表达之前先捋清目的和听众的视角很重要。我发现很多工作无从下手的原因是,我不清楚对方想要什么。比方说写工作报告,报告上应该体现什么呢?这些数据和描述会被用作考核的依据吗?如果能明白目的就容易多了(最怕的就是布置任务的人自己也不知道目的是什么啊……)。

回到编程语言的文档这个话题。作者用《PHP: The Complete Reference》和 Rust 的官方文档对比,我其实觉得不太公平,前者也不是 PHP 的官方文档啊。Rust 文档的开头其实就把目的明确了。

This book assumes that you’ve written code in another programming language, but it doesn’t make any assumptions about which one. We’ve tried to make the material broadly accessible to those from a wide variety of programming backgrounds. We don’t spend a lot of time talking about what programming is or how to think about it. If you’re entirely new to programming, you would be better served by reading a book that specifically provides an introduction to programming.

简单来说,这本书假设读者已经用其他编程语言写过代码了,有基础的编程知识,本身就不是写给初学者的。他们只是在明确了目的之后才写出了对初学者来说有点难读的文档。作者最多可以批评他们的目的有问题,但我的看法是,如果真的希望从零开始,应该去找一本有着更详细的解释和更多例子的书。

目的或许是表达者/编写者和读者之间达成理解,最重要的因素之一。


星群

r11y

把 HTML 转换为 Markdown 的库,可以用在任何网页上,能够去除掉非文本的其他元素。这个库用 Clojure 编写,同时也是一个命令行工具,可以在终端执行 r11y <url> 获取网页的 Markdown,用来写脚本或许很有用。

r11y 是 readability(可读性)的缩写,作者表示也可以读成:Oh, really?

访问: dazld/r11y


切片

  • 偷了伏枥的 着重号样式 来用。之前一直用 text-emphasis 实现着重号,但这会让有着重号的那一行的行高变高,看起来很奇怪,我一直没找到规避方案。这个基于 background-image 的方案看起来清爽多了。

    脚注的样式也修改了,字体调大了一些2,把数字编号从 1. 的格式换成了 [1],貌似也更像纸质书里的脚注格式。此外,脚注原本被刻意设计成透明度很高,不引起注意,在需要阅读时将鼠标移动到上面才恢复不透明度;我意识到自己没有考虑到移动端,现在移动端的小屏幕上是完全不透明的。

  • 因为某些我如今不想回忆的原因,某天早上一拳砸在健身垫上发泄愤怒,结果从那个时候开始直到现在小拇指都很痛,痛了两三天之后有好转,但我仍然觉得很不对劲,而且小拇指关节处有淤青。担心是骨折了,于是周六去挂了骨科的号。

    本来去了校医院,结果放射科没上班又跑隔壁医院,结果早上的号挂满了只能挂下午的,结果拍完片子要等结果,等完结果复诊又要排队…… 于是一天就这么过去了。

    还好没有骨折,可能是韧带拉伤了。谁能想到 618 的第一笔消费是 X 光和药钱呢?

  • 我第一个比较正经的手摇磨豆机是 MAVO 的「巫师 2.0」,用了快两年,感觉还行,唯一的缺点是刻度不好调,我每次都会忘记它当下是什么刻度,必须归零再数格子。太麻烦了,所以我把它调整到一个不错的手冲通用刻度之后,就再也没有调整过(除非花大价钱买了精贵的豆子)。

    凑到还不错的优惠券之后,我就下单了同样是 MAVO 的「幻刺 Pro」,貌似圈内风评很好,也算是观望已久的产品。收到货之后基本满意,外置刻度真的太好用了,也方便我记录不同豆子适合的具体刻度数字,很大的体验提升。

    两个黑色的手摇咖啡磨豆机,整体呈圆柱形,手柄收纳在侧边;左边的一个更高,带有外置的刻度调节组件,还有硅胶防滑垫,看起来细节更丰富;右边的一个更简洁,除了可以拧开的粉仓之外别无他物。

    左:幻刺 Pro;右:巫师 2.0

    包装盒的第一层,放着说明书、收纳袋和清洁刷。

    还不错的包装,不过那个刷子太硬太难用了。

    包装盒的第二层,机身和手柄被分开包装,放在两个凹槽里。

    顺带一提,图上的手柄收纳是 3D 打印件,是单独购买的。闲置的时候手柄不会因为惯性转起来打到东西,个人感觉非常有用。

  • 本周星露谷高光时刻。

    星露谷游戏界面,玩家在洞穴里,打开了宝箱,在其中发现了一个星之果实。对话框里的内容是:“你发现了一个星之果实!你脑中充满了各种想法,是关于 Boy Butt。你的最大能量等级提升了。

    你觉得我会在「最喜欢的东西」里填什么?

    不过矿洞是朋友探的,我只是去领了个果子。

  • 本周星露谷高光时刻 II。

    星露谷游戏界面,图中显示的是玩家和 NPC 的好感度列表,其中「谢恩」的好感度是八颗心,名字下方显示的是“男朋友”。

    谢恩,你究竟有什么魅力……

  • 我讨厌我的二十岁。


  1. 简单的内容管理和发布应用,一般用于把一串较长的文字内容复制粘贴到里面,然后发送公开的 URL 给别人看。如果内容比较长的话,可能不方便在邮件或即时通讯界面里发送,发送链接会方便很多。 Pastebin 是一个代表。 ↩︎

  2. 这是因为读到了 Cytrogen 的想法 。 ↩︎

堕落 流放与王国

2026年6月7日 14:48

总觉得加缪笔下的人物都有极其隐秘和个人,乃至诡异的行事动机,很难揣摩。若是读者的私人情感能与小说人物的行为对上,想必会非常喜爱,不过我必须承认,这部小说集里有一两篇我完全不能理解,比如最后一篇《生长的石头》。即便如此,读到小说人物依照自己的意愿做其他人都感到费解的事情,依旧感慨。

这部集子有两部分,第一部分是题为《堕落》的中篇小说,第二部分是《流放与王国》,由六部短篇组成。

堕落

《堕落》的法语原文是 La Chute,可以指代物理意义上的掉落、坍塌,数额的下降,当然也有比喻意义上的堕落。

我读起来有些不适应,全文都是碎碎念一般的独白。其实加缪描写的是对话,但对话的倾听者在故事结尾之前都是不可见的(即便是结尾,倾听者说的话都没有被写下来),读者可以从字里行间猜测倾听者说了什么,也可以把自己当作倾听者,想象自己坐在酒馆、在街头漫步,旁边有一个人滔滔不绝地讲他的故事。

小说的主人公,也就是独白者,在一开始将自己的职业称作「法官-忏悔者」,表示会在之后解释具体的工作内容。对话连续了几天,看起来二人一直在酒馆之类的地方碰面。值得一提的是,一开始是主人公主动找倾听者谈话的。谈话的内容大体是讲述他过往的经历,穿插着他的见解和看法。

一个人连着几个晚上都在谈自己的故事,很容易给人留下这人很自以为是的印象,其实他自己也这么觉得。在他的描述里,他自己是既自恋又谦卑的。

的确,说到谦逊,我是无与伦比的。

说我从未爱过是不对的。至少在我的一生中是有一种伟大的爱情,其对象一直是我本人。

我倾向于认为,他的谦卑和礼貌一样,是在人群中的伪装,为谋得便利而存在。他还多次提到,人们都很喜欢他,甚至尊敬他,他也对人很好。

不过,他并不是个浅薄的人,他有一定的学识、有知识分子的反思意识。甚者,他其实有些太知识分子了,他会在对话中使用虚拟式未完成过去时,这是仅仅在极其讲究的书面文本中才会使用的法语时态,他自己也承认这是种令人讨厌的毛病。在独白中,他提到自己是学法的,这也反映了他的文化修养。

他的确有一些值得玩味的观点。

我总觉得我们的同胞有两大狂热:思想和通奸。1

我很清楚人们离不了统治别人和被别人服侍。每个人都需要奴隶,如同需要纯洁的空气一样…… 一句话,能够发怒而另一个人不能顶撞,这是根本的。

“人不能顶撞他父亲”,您知道这句话吗?从某种意义上说,这句话不可解释。在这个世界上,不顶撞他爱的人顶撞谁呢?但从另一种意义上说,它又是令人信服的。应该由一个人说了算,否则,任何一种道理都可以有另一种道理与之对立,这样就会没完没了。相反,实力解决一切。我们花了点时间,明白了这一点。例如,您该注意到,我们古老的欧洲终于用正确的方式来推究问题了。我们不再像幼稚时代那样说:“我这样想。您如何反驳?”

那么,自愿地死有什么用?为自己愿意有的关于自己的看法而轻生有什么用?您死了,他们则加以利用,对您的行动赋予一些愚蠢或庸俗的动机。亲爱的朋友,殉道者应该在被遗忘、被取笑或被利用之间进行选择。至于被理解,绝不可能。

我由于生活在人们中间但不赞同他们的利益,而不能够相信我们所承担的义务。我的礼貌、我的懒散,足以回答他们在职业、家庭、公民生活中对我的期待。

可以看出他不仅是知识分子,而是习惯于反思宏大命题、进行哲学思考的知识分子。他思考的包括政治问题,当然也包括加缪笔下的「唯一严肃的哲学问题」——自杀。上述引文的最后一段,还让人联想到加缪的其他作品,比方说《 快乐的死 》,其中梅尔索就开了间他并不在乎的药店,以在人们面前建立起可辨识的身份,避免闲言碎语。

这么看来,主人公还只是一个表达欲旺盛的知识分子而已。他之所以在酒馆里跟同一个人一夜接着一夜地聊,据他所言,是他看出来倾听者和他是一类人。的确,倾听者是位律师。

小说的大部分篇幅都是主人公的碎碎念,讲完自己的谦逊和受人欢迎之后,他又开始描述自己的失败和悔恨,对自己的不甘和对社会大众的怨恨,这大概是「堕落」的第一层含义了。之后,他不甘做法官,成了「法官-忏悔者」,对这个职业的解释,他仍准备之后再谈,他只说这个职业他无时无刻都在工作,哪怕是在床上睡觉的时候。

习惯上,我的事务所在墨西哥城。但是,伟大的使命超出工作地点。甚至在床上,甚至在发烧的时候,我都工作。何况,这种职业,简直不是干,而是时时刻刻在呼吸着它。

此时这篇小说的存在主义意味就已经很明显了,一方面是主人公呈现出了与社会大众在价值观层面的割裂,另一方面是他树立起了新的价值、新的职业标签和对职业的新定义。不过,他可能比我一开始想的还要神叨叨一点,后文会回顾他的「职业身份」。

当然,这时倾听者也觉得面前这人在胡扯了。读者可以从主人公的反应看出来(因为倾听者的话没有被记述下来):

我知道你想什么:从我的叙述中分辨真伪很困难,我承认你想得有道理。

主人公是这样为自己辩解的(尽管是他自己提出的驳论点,接着它又因自己提出的论点而辩护):

说到底,这又有什么关系呢?谎言最后不也通向真理吗?而我的故事,或真或假,不是都朝着同样的结局、具有同样的意义吗?如果在两种情况下,他们都表明了我过去是什么人,现在是什么人,它们是真是假又有何妨呢?有时候,人们看一个说谎的比看一个说真话的还要清楚呢。真相,如同光亮,眩人眼目。谎言则相反,是一抹美丽的霞光,它使每样东西都显出价值。随您怎么看,反正我曾在一个俘虏营里被委任为教皇。

他也的确讲了自己做教皇的故事,那是一群出于绝境中的人做出的决定,他们需要一个人来分配水资源,他被选上是因为他自认为是这群人中弱点最多的,等等。故事讲得没头没尾的,很快又扯到别的话题上了(这也是为什么我读起来很不适应)。

小说最后主人才解释了「法官-忏悔者」的真实含义。他忏悔,然后审判。在五六天的谈话当中,他一直在吐露自己的过去,可能有不少谎话,但至少他认为这表明了他真实的样子,他暴露了在某种意义上真实的缺陷、过错和痛苦——他是个忏悔者。

但他又没有把自己放在低贱的位置上,他仍认为自己高尚和谦卑,他滔滔不绝地讲道理、逻辑和理性,这看起来倒像个法官。法官和忏悔者的身份是怎么联系起来的呢?

我肯定。我越是认罪,越是有权审判你们。

主人公承认他接近倾听者,是因为他看出倾听者与他是同类人,也是背负着罪恶的知识分子,所以他要向他忏悔,再借此审判他。他审判的对象其实是全人类,因为他作为法官,相信没有人是无辜的。这和基督教的「原罪」还不一样,倒是有点像加缪的另一篇小说《 鼠疫 》里描述的病症——所有人的身上都潜藏着鼠疫,“可鼠疫究竟是怎么回事?那就是生活,如此而已。”

这位法官-忏悔者认为,人存在于世,互相干涉,就是一种罪——至少我只能这么理解。换句话说,这是存在主义的原罪,人们在意识到这一罪行之后开始堕落。

不贞的妻子

平淡的婚姻生活的切片,主人公是妻子。加缪笔下的女性角色也十分细腻,但不庸俗,或许可以说是自我意识很强烈吧。这篇小说写的是自我意识强烈却找不到出路,被困在没有爱情的婚姻里的妻子。小说并没有什么夸张的情节,没有女人决定逃离的爽文情节,相反,我觉得这篇小说更加「真实」,大部分人其实逃不出困住自己的枷锁,只有情感是强烈且真实的。

加缪借着笔下女人的视角,这样描述一部分男人:

平日里他们装出通情达理的样子,到时候就发起疯来,绝望地扑向一个女人,为了在女人身上埋藏他们因孤独、黑夜而产生的恐惧。其实他们并没有欲望。

妻子雅妮娜自己也是这样,她知道自己离不开丈夫马塞尔是因为害怕孤独。小说中她依偎着丈夫睡觉,仅仅是为了缓解孤独和痛苦,直到她意识到有一部分空缺是马塞尔无论如何也填补不了的,那个时候他半夜跑出了旅馆。

尽管题为《不贞的妻子》,雅妮娜不仅没有在性的层面出轨,也没有在精神层面对另一个人产生依恋。丈夫是兜售布匹的商人,她仅仅是随行,在公交车上,她遇到了一个对他微笑、给他递糖的法国士兵,她接受着士兵的示好,内心有些慌乱,但并无出格的想法。下车之后,士兵却像没看见她一样在她身边走过。读者其实不难想到,在车上,马塞尔对雅妮娜一直十分冷漠,大部分时候一言不发、一动也不动,士兵在他们下车之后才意识到,雅妮娜已经有主了。

她也没有因此沮丧,因为她并没有什么出格的想法,她仅仅是接受了好意而已。这之后,她随着丈夫去旅馆看房间,然后陪着他销售布匹,感受着他心情的起落,在他的货出手之后,她才发现丈夫高兴起来。这时雅妮娜想去城堡上眺望沙漠,马塞尔先是说想要回房间,后来才笑着说:「当然是陪你啦,亲爱的」。这一变化十分微妙。

雅妮娜在城堡顶上看沙漠时,马塞尔一直心不在焉,不觉得沙子有什么好看的。之后他们离开了,在房间里因害怕孤独而依偎在一起。半夜雅妮娜偷跑了出去,她去实施她的「不贞」了——她跑到城堡顶上,去眺望沙漠,感受夜气。我觉得她是在借着景象感受自己的贫瘠的婚姻和荒芜的生命,被困在身体里无处释放和流动的生命力。

最后她回到房间,丈夫醒来时看到她止不住地哭,丈夫觉得莫名其妙,问她怎么了,她只说:

没什么,亲爱的。没什么。

小说仅仅是他们婚姻生活的切片。除了两个人互不理解、无法在陪伴之外的层面提供更多的情感需求,这一浅显的典型关系之外,小说还指出了一个关乎所有现代人的社会事实:不是所有人都能承受得了自由。

雅妮娜渴望逃离婚姻,但她没有目的地。困住她的并不是父权制,并不是社会对女性的偏见,而是迷茫,她不知道该去哪里,不知道自己是谁。想要摆脱现状却无法找到另一个可以作为目的地的境况,是很多人的困境。人们都有选择的自由,没有人是被逼无奈的,但事实就是,人们在拥有自由的同时不去行使自由,因为自由难以承受。

叛教者:或一个精神错乱的人

和《堕落》一样几乎是独白的小说,不过并非是对话。主人公是信仰上帝(应该是基督教)的人,处于某些原因去到了盐城,在路上被司机打了一顿,到城里之后还被奴役了。小说中的盐城是个落后的部落,主人公可能是去传教的,那里的人们有完全不同的宗教信仰。

这篇小说有着「不可靠的叙述者」,开始独白时,主人公貌似已经精神错乱了。他已经叛教,表示他在等待传教者到来,然后把他干掉。

他在盐城的经历,先是被关在狭小、黑暗的房间里,吃喝拉撒都在里面解决,他只能用手挖土坑来排泄。之后,巫师带着一群人进来跳舞,应该是在作法,随后他被带走,扒光衣服,让他崇拜偶像(这里的偶像应该是他们信仰的神明的雕像)。巫师大概和神甫是类似的角色,不过在这个小部落有更高的权力,几乎是统治者。

值得一提的是,主人公和当地人语言不通,这也是叙述者不可靠的原因之一。其他人的意图和行为,他都只能揣测。由于被关了好几天,还被扒光衣服一顿毒打,他貌似已经精神错乱,他把自己当作效忠于偶像的、与他们平起平坐的兄弟,他之后的生活一直是在偶像旁边打扫、干活。

之后的某次仪式,巫师带进来一个女人,在他面前把双腿掰开,接下来就毒打他下面。最后,他因为自己的性欲受到惩罚,舌头被活生生拔了下来,他还描述了舌头撕裂的感觉。他本来就因语言不通无法被理解,没了舌头之后的胡言乱语,对他们貌似也没差别。

他一直在接受各种恶意。比起叛教者,他更像堕落者。他的确信仰偶像了,不过他把偶像当作「恶」的象征,在精神错乱的独白中,他认为「恶是更好的统治」。他一直以为他在和“兄弟们”践行恶的美德,直到最后读者才知道,他实际上是被部落当作了奴隶,而奴隶不该说话也不配繁衍后代。

他的确也堕落了,在看到传教士到来和法师谈话时,他听到传教士和军队会尊敬当地风俗,但貌似是会采取某些管理措施。他非常愤怒,于是深夜他偷跑出去,偷了枪,一枪毙了传教士。

之后他见到巫师,开始大声赞颂偶像,结果:

一把盐塞住了饶舌的奴隶的嘴。

小说让读者见证了一个人发疯的全过程,并非是他本身就有疯狂存在于身体里,而是他因为他接收到的只有恶意,恶成了他所能人认知到的世界的全部,最后甚至投身于恶。又由于语言不通,他无法和部落里的人相互理解,疯可能是不被理解的恶,而恶最终来自不理解的人本身。

另外,我挺喜欢这个结局的,他终于闭嘴了,好吵

沉默的人们

和《不贞的妻子》类似,是平常生活的一段切片,故事发生在制桶工人之间。我倒是第一次读到加缪关心具体的人间疾苦,不过制桶工人也在《快乐的死》里出现过,在他的时代应该是常见的职业吧。小说中写道,制桶是精细的手艺活,而且很难,需要花很多时间才能学好学精。不幸的是,制桶业变得不景气了,人们更愿意用旧桶而不买新桶,工厂老板赚不到钱,发不出多少工资,工人们罢工失败,和老板的关系闹得很僵。

职业是个好职业,可就是没有出路,人被卡死了,只好忍气吞声。然而忍气吞声也不容易,难的是要闭上嘴,不能正经地讨价还价,每天早晨去上班,越来越累,到了周末,人家爱给多少就领多少,而那点钱是越来越不够用了。

现在的许多人似乎也经历着同样的困境呢。

小说最引人深思的是,制桶厂的老板并不是坏人,在经济问题出现之前,他和员工的关系很好,和蔼可亲,还经常一起聚餐。可钱的问题出现时,他说了不少过分的话,比如「爱干不干」和「我把厂关了还更省钱呢」。他之后想要和工人们把话说开,可厂里的气氛非常冷,他常常在房间里走过,却没说几句话。生意不好他也没办法。

小说最后,一个年轻的工人摔倒进了医院,一句有来有回的询问过后,工头只是让大家收工,此时大家已经说不出话来了。

《沉默的人们》并不像标题预示的那样,人们为了自保而不敢于权威挑战。实际上,工人们试过罢工,态度坚决,最后却被工会劝和,没有结果。他们还和老板私下对峙,自然也是无果。直到同事受伤的惨剧发生时,他们已经没有心力了。沉默并不是选择,而是心力交瘁和无能为力的结果。

这是不是现在程序员看到离职同事变成 Skill 的感受呢?

来客

主人公达吕是山里的小学教师,大雪封山的一天没有学生来上课。晚上,警察巴尔杜克西把一个阿拉伯犯人带来,要他押送,并告知这是命令。他给犯人松了绑,沏茶做饭,还铺了床。他告诉警察,他不会把他交出去的,被赶出去时,警察只是要他签字,表明他已经把人送到了。

阿拉伯人是个杀人犯,达吕不同情他,他对其罪行当然感到愤怒,他对所有暴力的行为都感到愤怒。可他并没有跟他对峙,甚至没有一点防备心,他把警察留给他的枪放进抽屉里,裸着身子和犯人睡在一个房间里。半夜他看到犯人起身走出去,一会儿又回来了。

第二天,迟疑过后他仍然没有带枪,把犯人押到一个路口,给了他一千法郎、食物和水,告诉他东边是政府和警察局,南边是可以接待和保护他的游牧人。他把犯人的身子往南的方向转,可再回头看时,犯人已经走在了去监狱的路上。他心头一紧。

达吕的动机对我而言很难揣摩,他明显不想让犯人受牢狱之灾,而他的确厌恶他的残暴,为什么会这样矛盾呢?我只能结合他的教师身份解释,或许他想改变他,给他机会。或许他厌恶统治这片土地的人,不想让他们去审判犯人。

我不知道,或许也没必要知道。

在这片他如此热爱的广阔土地上,他是孤零零的。

约拿:或工作中的艺术家

画家的故事,约拿先是和画商签订了合同、娶了路易斯、租了个小房子,开始构建自己的生活。他还有个叫拉多的挚友。成名过后,有越来越多的人出现在他家的客厅,不是在接待客人就是在接电话。

一开始他过得很高兴,后来他发现自己绘画的时间变少了,陪伴妻子、孩子和朋友的时间也变少了。他还有了弟子,弟子对他要求非常严苛,强硬地表达了他们对约拿作品的看法,努力不让他偏离“正轨”。拉多觉得这是对他艺术的限制,他喜欢约拿的作品,仅仅是因为那是他的艺术。约拿没有听,之后越来越多的人涌入,在他绘画的过程中作评论。由于名声越来越大,甚至还有别的画家在他作画时为他画像,画名叫《工作中的艺术家》。

很快,他的生活变得糟糕,他自己的时间越来越少,作品也变少了,批评也开始出现。他尝试改变,接待更少的人,得到的评价却是:他成名之后骄傲了,谁也不见了。恶性循环逐渐变得越来越坏,他的艺术也变得越来越糟,批评家开始出现。这之后,他无论如何也画不出来了,就算让自己独处,隔离所有声音,也无法再产出新的作品。画商那边也在减少月钱,妻子和朋友很是担心。

他搬了一块木板,在家里造了个阁楼,就关在上面画画。人们在下面找不见他,也没办法打扰,妻子也只是上去送饭。上面没有灯,他摸黑作画,其他人什么也看不见。最后,他累倒了,工作过度。拉多去阁楼上看,画布什么也没有,只有角落上写了非常小的字,看不清楚是 solitaire 还是 solidaire

这两个词的意思分别是「孤独的」和「友爱的」。

或许这是约拿想要的:孤独而友爱,而不是在人满为患的厅室里迷失。也可能是在描述叠加态,他试图对所有追随者友爱,可他仍觉得孤独(不过这个解释太庸俗了,也和小说不太能对得上)。抑或是一种二元对立,艺术家应该是孤独的,对所有人友爱会使他平庸。也可能是共存,艺术家应该保留大体上的孤独,把友爱留给家人和挚友。

看到有人说《约拿》可能是加缪笔下的自己,我对他的生平并不了解。我的书架上还有一本《加缪传》,或许该读一读了。

生长的石头

我完全没读懂的一篇。小说分两部分(我的印象里有两部分写法完全不同,并不是加缪自己划分了两部分),第一部分没有对白,只有描写,夜里二人驱车前往河边,下车后和另一波人接应,在水边和码头做了一些奇怪的操作,然后他们把车开上了码头,又开上了木筏…… 原来是要带车过河吗?

不过这里的描写还挺有意思:

大个子黑人不动了,双手举在头上,紧紧地握住浅浅地插入水中的船蒿的一端,筋肉紧绷着,瑟瑟地抖着,那颤抖就像是来自水中,来自水的压力

我想这是没有航行过的人写不出来的话,水波抖动的力量比人想象的要大。

汽车缓缓地爬上斜坡,车灯直指天空,又指向河面,开始下坡了。

我喜欢这里车灯的描写。

第二部分是主人公到达了小镇,这才揭示了他们的身份。他们是工程师,过来帮忙修建水坝的,受到了市长和各种名流的接待。这个地方很穷,他们也有风俗和节日。小说的大部分笔墨都放在主角达拉斯特和镇民的交流上,修坝工作则略过了。

他们的风俗之一是去某个山洞里轮流砸石头。据说之前有耶稣像逆流而上来到这里,他们捡起来之后放进山洞里,之后他们每年都去砸石头,但每年石头都会长回来。另外的风俗是某个跳舞的节日,房间里所有人都聚集在一起,像鬼神上身一样跳舞。

小说中还有很多令人费解且并没有得到解答的情节,比方说刚来到镇上时,醉醺醺的警察局局长冲上来,要求达拉斯特出示护照,并声称护照有问题,最后局长被市长打发走了。本来还和达拉斯特聊得很好的厨子,在舞蹈过后突然变脸把他打发走,就像不认识他一样。跳舞的时候,人们一开始像被附身了一样颤抖,之后又像是睡着了,意识模糊、跟不上节奏地胡乱摇摆,这些舞蹈的含义也没有得到解答。

唯一有迹可循的线索是,厨子说他立下誓言,要在第二天把一块石头顶在头上,从山洞顶到教堂前面。他照做了,不过跳舞跳了一个通宵,累得不行,在快到教堂时再也动不了了,一边哭一边倚在达拉斯特身上。达拉斯特接替他顶起了石头,但他并没有去教堂,而是把石头一路顶到了厨子家,把石头放在了屋子中间灶火的位置上。

其他人跟着进来了,没有看他,在石头旁边围着坐下了。厨子的兄弟要他加入,一起坐下来。

我本以为我写完了会得出什么结论,不,我还是没读懂。或许这些东西没什么可被作者本人以外的人解释的象征含义,他们存在于故事中,仅此而已。哇,这可真是够存在主义的。

那么就这样吧。


  1. 法国人自己也这么觉得吗? ↩︎

回归晨跑

2026年6月5日 16:52

去年秋冬我丢掉了跑步的习惯,我怪罪天气,不喜欢在寒风中大汗淋漓。冬天的重庆,气温很尴尬,跑者社群里常谈的「三层穿衣法」根本不适用,而穿少了又会冷。今年初夏,确实没理由不跑了,正好也想要调整作息,我决定开始晨跑。

去年读了一本很棒的有关生物钟的书,叫作《 绝佳时间 》。尽管掌握了不少和昼夜节律相关的知识,我并没有很好地实践。我晚上总是不困,总是在等待某天半夜会突然产生睡意,可结果是,我每天都在凌晨一两点才被紧张感驱动着上床,随后为了保证八小时睡眠,把闹钟调到了十点(前提是第二天早上没有安排)。

我试着减少光照,减少夜间的屏幕时间,在睡前看书等等,都无法让我睡着。更可怕的是,我感觉一天格外地短,我状态最好的早晨在睡梦中度过,糟糕的下午被困在教室,摸鱼也只能完成一些琐事,到了晚上,因为状态太差,要么在电视剧集里逃避现实,要么在《饥荒》和《星露谷物语》里逃避现实——最差的时候,我甚至在睡前刷短视频,那应该是我的第一个警钟。还好我没有刷充斥着蹩脚音效的垃圾短片,我只是一直在看别人养的狗狗和别人的生活,不过那也很糟糕。

既然从晚上下手行不通(毕竟夜间意志力薄弱是难免的),那就从白天开始调整吧。我回忆起《绝佳时间》里的知识,昼夜节律的重要调节信号是光照,而且是自然光(人造光远远达不到调节生物钟的效果,在夜间使用较强的人造光也只是会提高警觉性,对生物钟的影响微乎其微),而在室内,窗户会减弱光照,再加上并不是所有人的家里的采光都足够,所以在早晨出门接收光照很有必要。晨光会让生物钟前移,更接近太阳东升西落的节律,夜间的强光则相反。

如果只是出门走走,我会觉得有些浪费时间,但如果是锻炼身体的话,就是一举两得了。所以某一天我设置了七点的闹钟,尽管前一天晚上我一点多才睡,但我还是强制开机,让自己爬起来了。强制开机并不痛苦,关键在于醒来的时机,如果刚好在快速眼动睡眠期(REM)醒来,就会感到神清气爽,即便总睡眠时长可能只有四五个小时。这是因为 REM 是睡眠周期的「交界点」,是前一个周期开始、后一个周期结束的阶段。

我记得 REM 过后,人本身就会醒来,一个晚上人会从睡梦中醒来几次,只不过忘记了而已。夜间醒来,哪怕是有意识地去做了某些事情,也很容易被忘记。我记得有一次我在半夜突然流鼻血,醒来之后察觉异样,看到手上全是血,摸黑找纸巾收拾之后我很快就睡了。第二天我在洗漱时,从嘴里吐出了带血丝的痰液,疑惑片刻后才想起前一天晚上的事情。

我跑题了,总而言之,如果有唤醒方式更轻柔的闹钟,让自己在一个睡眠周期结束过后自然醒来,早起就没那么痛苦。

晨跑的第一天,我去了家附近的公园,发现那里已经有很多跑者了。这个时候阳光暖暖的,不算炎热,在树荫下的空气也很舒服。更重要的是,不像晚上,白天不会有并排散步的老人、遛狗的人和牵手的情侣,这些人可能不认识地面上的标识吧,或者根本不在乎,肆无忌惮地在跑步道上慢悠悠地走着,慢跑变成了障碍跑,还有惊险刺激的躲避机动车的环节。

尽管几个月没有跑步,但有氧我并没有落下,一直有在做心肺锻炼,呼吸控制也内化于心,节奏把握得不错。就着这股劲儿的话,跑个三公里甚至更长,是没有问题的。

可是刚跑过一公里,我就不得不停下了。

这种感觉很奇怪,我的心肺好得很,心率保持在 2~3 区间,呼吸也没有乱,我也不觉得饿或者疲惫,但就是跑不下去了。我身体的其他部位都很正常,唯独脚踝在不争气地抗议——痛,为什么会这么痛?

我以为是这天状态不好,拉伸放松过后就回去了。可第二天,同样的事情还是发生了,我的脚踝在一公里之后发送了剧烈的痛疼信号,让我怀疑再坚持下去脚就要坏掉了。第三天,我吸取教训,做了动态拉伸,和基本的热身,情况并没有变好。第四天也是一样。每次都是这样,我的身体渴求更多的刺激,但只有脚踝要求必须停下。这就是木桶效应吧,我以前非常讨厌这个词,现在它来找我算账了。

如果在跑步两三公里之后就感到力竭,我会在下次调整配速,控制心率;如果跑得上气不接下气,感到整个身体都在反抗,我会刻意调整呼吸。在跑步中遇到的各种问题,大部分都像「咖啡风味不明显,粉要磨得粗一点」一样,能快速找到解决方案。这种症状对我来说是全新的:为什么脚踝会痛?为什么以前从来没痛过?是什么导致的?我要怎么解决?

是体重吗?经过春节和被焦虑、抑郁情绪侵袭的春天之后,我的体重的确有了明显的增长。从我高考后减掉四十斤起,这是我第一次长胖,接近三年的时间里我都没有这种幅度的反弹(甚至还又瘦了二十斤)。长胖的期间我没有跑步,没有让脚踝渐进地适应体重增加,所以才晨跑时感到力不从心吗?这么说来,脚踝的确承受了太多它不该承受的重量。

可之前我两百斤时,也每天跑步(大基数的读者不要学,这对膝盖冲击很大),那个时候并不觉得脚踝痛。有没有可能是那时的我并不懂得感受和解读身体信号呢?

前几天听了树老师的播客《 如何爱上跑步 》,她从她的视角讲述了国内应试教育体系下的跑步是什么样的,这又对她关于跑步的认识产生了怎样的影响。中国的初高中尽管有体育这项考试科目,但体育的排课少得可怜,大部分时间里学生都不会跑步,也不被鼓励锻炼身体,除了跑操,但我们都知道让一群身体机能差异巨大的人挤在一起、整齐划一地跑步,有多么反人类,跑操的目的是完成“体育教育”的责任,以及装样子给领导看。

树老师说她对跑步的认识就是:在考试的时候拼了命地跑,忍耐跑步过程中的所有痛苦,忍个几分钟就结束了,然后就能拿到好成绩。荒谬的是,在规律跑步训练缺失的情况下,学校指望学生在突然出现的 800 米或一千米的考试中取得好成绩。这不仅不合理,而且危险(除非你是大学生,那不锻炼还抱怨体测就是你自己的问题,你是拥有大把自由时间的成年人了)。

之前的我对于跑步也是这样的认识吗?我是不是习惯了忍受痛苦,以至于忘记了跑步后脚踝也会痛呢?

另一种可能是我忘记了正确的跑步姿势,就像力量训练过后斜方肌疼痛,是因为姿势有误而导致的肌肉代偿,仔细思考的话,跑步时落脚的位置和腿的运动方式若是有问题,冲击力也会过多地让脚踝承受吧。我的确没有刻意练习过跑步姿势,就像稀里糊涂地骑自行车骑了几十米却没摔倒,也不代表自己就会骑车了,下次上车可能就蹬不动轮子了。我太久没跑步,回忆不起之前的跑步姿势了?

说到这里,让我再跑个题。在一两年前,我和同室(那时还没有合租)一起跑步过一次。他没有跑步习惯,当时的我已经坚持跑步快一年了,我刻意控制配速跑得很稳,他跟在我后面,突然问我:「你是在让着我吗?」我说没有,接下来他就跨着大步子往前面跑了。我印象很深刻,他跨起步子跑的时候,整个人都跳起在空中了——顺带一提,他是白羊座。

任何有长期跑步习惯的人都知道,小步快跑更省力也更安全,腿和地面的夹角太小的话,关节受到的冲击力也更大。我观察到,几乎每一个没有跑步习惯的人,在和我一起跑步时都会超过我,然后,他们也会更快地停下,先我一步离开运动场,因为他们坚持不下去了。

这或许是应试教育迫害锻炼的另一种形式:无处不在的内卷。也可能是某种原始的好胜心,总之都挺蠢的

许多人并不会感受和分析身体信号,就像养宠物的人自己也很可能注意不到宠物发出的信号。比方说狗狗不断地舔嘴是一种安定信号,此时它可能已经很焦虑了,再刺激的话可能会有应激反应。人在半夜想吃零食,很有可能是口渴,或者是用脑后自然地渴望补充糖分,此时等待肝脏分解糖原就好了。对于不熟悉的场景,感受身体信号就更需要练习,就比如说跑步时的各种不适和痛感,都应该有原因和规避方法。

不过,或许也应该从外部找找原因了。有没有可能是因为路面不平,导致脚落地时受力不均,脚踝被轻微扭伤了,才觉得疼痛呢?公园的道路的确起起伏伏,每跨一步都觉得落点的地面形状不太一样。再者,尽管挡路的人变少了,但也不是没有,频繁地变道、降速,甚至停下,也会打乱我跑步的节奏。

我接下来要做的是换一个地面更平、更适合跑步的场地,同时也做一些提踵练习。希望用不了多久,我就能在早上体会到久违的酣畅淋漓吧。

跑步本身令人失望,但我的节律的确调回来了,我已经会在晚上十一点自然地打哈欠,也不会因为一天太短而感到不甘心,睡眠质量在短短的几天内就恢复了。

晨跑之后,我突然又能看进书了。今年年初信誓旦旦地立下了 40 本书的目标,去年只差两本,想着今年应该不会太差,结果是焦虑、抑郁、痛苦、逃避…… 总之状态一直很差,读书计划也一推再推,进展缓慢。这几天我都会在晨跑回来之后洗个澡,然后让自己安心读书,进度意外地不错。运动后注意力会更集中,而且一整天都能感受到全身散发着一种难以言喻的舒适感,整个人会更有精神。我想下一期书评应该不远了。

看来不能等待自己情绪变好再努力让生活变好,应该尽力让生活回归秩序,尤其是要保障睡眠和调整节律。说起来有些神奇,但我逃避找工作逃避了两个月,在我开始晨跑之后的两天,我就十分自然地开始投递简历了(虽然这些 HR 要么是一直未读,要么是已读不回,但我才投几份啊,问题不大),一两个月前深不见底的焦虑情绪似乎消失了(但还是别太早下定论……)。总之,如果只是被动地等待改变发生,那么改变永远也不会发生。

再写就变成鸡汤了,所以点到为止吧。回见。

从 CLOS 审视 Java 面向对象编程

2026年6月4日 19:23

书接 上回 ,前一篇文章讲了 Lisp 中的基本数据结构列表的构造方式(cons pair),以及由此衍生出的关联列表(alist)和属性列表(plist),之后我就跑偏了,谈起了编译器和解释器中的「特例」以及宏,为什么看起来像是函数的形式实际上不是也不能是函数,最后浅谈了函数式编程和 let 的实现方式。

今天的文章继续跑偏,来关注 Common Lisp 的另一部分语言特性,即面向对象编程,也借此来谈谈我对 OOP 多少有些复杂的看法。我将逐个审视 OOP 的三大要素——多态、封装和继承——再来反思面向对象设计模式。

不属于类的多态

Common Lisp 在这点上与 Clojure 类似,提供了非常相似的 defgeneric(CL)和 defmulti(Clojure),它们都能定义「抽象函数」,也就是不包含具体实现的函数。之后可以使用 defmethod(两门语言相同)定义方法,也就是抽象函数的具体实现。

以下是来自 Common Lisp 文档 的例子:

(defgeneric description (object)
 (:documentation "Return a description of an object."))

(defmethod description ((object integer))
 (format nil "The integer ~D" object))

(defmethod description ((object float))
 (format nil "The float ~3,3f" object))

defgeneric 定义了一个名为 description(描述)的抽象函数,或者用 CL 的术语来说,应该叫作泛函数(generic function)。后面的代码用 defmethod 定义了两个 description 的具体实现(或者说方法),分别对应整数型(integer)和浮点型(float)。这两个 description 方法共享一个符号名,调用时没有任何区别,但根据传入的数据类型不同,被实际调用的方法也不同。

(description 10)
;; => "The integer 10"

(description 3.14)
;; => "The float 3.140"

此时还没有类参与,如你所见,CLOS1 中的方法不属于类。这只是多态(polymorphism)而已,其实 Java 等面向对象编程语言的多态也不需要类参与,只不过 Java 里的方法必须放在类里。

public class Descriptor {
 public void description(int number) {
 System.out.format("The integer %d", number);
 }

 public void description(float number) {
 System.out.format("The float %f", number);
 }
}
descriptor.description(10)
// => "The integer 10"

descriptor.description(3.14)
// => "The float 3.14"

当然,上面这段代码是违反开闭原则的,如果要添加新的类型,就需要修改 Descriptor 类。大部分情况下,Java 程序其实长这样:

public interface Descriptor {
 void description();
}
public class IntegerDescriptor implements Descriptor {
 private int number;
 public void description() {
 System.out.format("The integer %d", number);
 }
}
public class FloatDescriptor implements Descriptor {
 private float number;
 public void description() {
 System.out.format("The float %f", number);
 }
}

如此一来,添加新的类型只需要添加新的 Descriptor 实现类,程序对修改关闭,对拓展开放。看起来很美好,不过后果是我们写了三个文件,因为 Java 限制一个公开类只能放在一个文件里。并且,调用 description 方法时还需要先创建对应的 Descriptor 对象,把要描述的数据写入这个对象的成员变量中,然后再调用 description 方法。

而 Common Lisp 这边只需要写 (description data) 就好了,方法定义可以放在一起,也可以不放在一起,还可以在运行时动态地添加新的方法定义。

(defmethod description ((x integer) y)
 1)

(description 1 2.0)
;; => 1 

(defmethod description ((x integer) (y float))
 2)

(description 1 2.0)
;; => 2

上述代码并没有覆盖原先的方法,而是在名为 description 的方法集中添加了新的方法。尽管方法调用的参数完全一致,但在运行时添加了新的方法定义之后,这个方法调用被分发到了新的定义上。听起来很酷,不过看起来也容易出现时序耦合之类的问题,而且也失去了编译时的保护,需要谨慎使用,要保证系统的行为是可预测的。

这是把类和方法分开的好处之一,由于方法不属于类,程序员不总是需要先实例化类再调用方法(除非是静态方法,但很少用),也不需要把方法都放在同一个地方,在编译之后还能动态地添加新的方法。

再者,CLOS 是多分发(multiple dispatch)的系统,一般的 OOP 语言是单分发的。例如在 Java 中,我们只能根据对象的类型这单个“参数”分发方法,floatDescriptor.description() 对应一个方法,integerDescriptor.description() 对应另一个方法;在 CLOS 中,方法不绑定在对象上,只要多个参数中有一个参数的类型不同,都被视作不同的态,比如 (description 1 1) 对应一个方法,(description 1 1.0) 对应另一个方法。

以上大体是 Wikipedia 和一些 Reddit 用户的观点,他们貌似忽略了 Java 也可以在方法的参数层面做到多分发,在一个类中可以定义多个名字一样但形参不同的方法。调用类的方法时,先是通过对象的类型进行单分发,然后再根据形参进行多分发。不过我也在前面提到了,由于 Java 的语言限制,方法必须和类绑定且必须定义在类的内部,依赖多分发,容易违背开闭原则,所以 Java 编程一般使用基于类的分发,忽略了基于函数签名的多分发,况且在静态 OOP 语言里通过在运行时传入类型不同的参数来分发方法,听起来有些大逆不道

尽管最后得出的结论类似,但我想有必要澄清一下:Java 的确支持多分发,而且有一个专门的设计模式就利用了这个特性,叫作 Double Dispatch (双分发)。

一边在学校被迫学习 Java 设计模式,一边自己摸索 Lisp 编程,我愈发觉得各种面向接口编程和花里胡哨的设计模式,其实都是对 Java 语言限制的妥协。这是一门很经典的 OOP 语言,但它不够灵活,而且啰唆,往往要创建很多类,以提升系统复杂度为代价去满足设计原则,但实际上这些原则可以在其他语言里用更简单的方式被满足。

封装与封闭的两难

讲完多态,接下来是 OOP 的第二个要点:封装。

前文提到 Java 的啰唆、复杂是其语言限制导致的,具体来说,这个限制就是严格的封装(encapsulation)以及封闭类(closed class)。请允许我用长篇大论批评 Java 这门语言,然后再来对比 Common Lisp 的做法。

To encapsulate, or to indulge?

所有 Java 程序员应该都写过在我如今看来依旧非常神经的 getter 和 setter 方法,也就是定义一个类和它的私有成员变量,保证外部类无法直接访问其变量,只能调用它的公开方法设置和获取变量,以此避免破坏类的封装性。所谓的不能破坏封装性,就是防止外部类用意想不到的方式访问其数据,比方说,商品类中包含价格这一变量,价格不能为负数,如果价格变量是公开的,那么外部类有可能把价格修改为负数,但如果不公开这个变量,而是公开 setPrice() 方法,那么就可以在这个方法里加上判断,如果传入的参数小于 0,就报错。

听起来好像很美好,但 Java 里的实体类基本上都长这样:

public class Employee {
 private Department department;
 private String name;
 private String ID;
 private Gender gender;
 private double salary;

 // --- Constructors ---

 public Employee(Department department, String name, String ID, Gender gender, double salary) {
 this.department = department;
 this.name = name;
 this.ID = ID;
 this.gender = gender;
 this.salary = salary;
 }

 public Employee() {
 }

 // --- Getters ---

 public Department getDepartment() {
 return department;
 }

 public String getName() {
 return name;
 }

 public String getID() {
 return ID;
 }

 public Gender getGender() {
 return gender;
 }

 public double getSalary() {
 return salary;
 }

 // --- Setters ---

 public void setDepartment(Department department) {
 this.department = department;
 }

 public void setName(String name) {
 this.name = name;
 }

 public void setID(String ID) {
 this.ID = ID;
 }

 public void setGender(Gender gender) {
 this.gender = gender;
 }

 public void setSalary(double salary) {
 this.salary = salary;
 }
}

难道 Java 程序员写这么多重复的方法不觉得烦人吗?——当然!所以他们爱用的 IDEA 里面有代码生成器,用来自动补全 getter 和 setter 方法,还可以生成包含指定参数的构造方法。我想他们使用 Alt + Insert + Enter 已经熟练到不觉得这有什么问题了。另外还有名为 Lombok 的库,提供了 @Data 注解自动生成这些东西,你知道这个用来生成模板代码的库还有付费的企业版吗?人们在花钱解决本就不该存在的问题。

你可能会说,不想这么做的话,把实体类的属性设置成 public 就好了,不提供 getter 和 setter 方法不行吗?

不行。这种拥有完整构造函数和 getter/setter 方法,封装良好的 Java 类,有一个专门的名字:Java Bean。主流的框架 Spring 就基于 Bean 构建(例如 BeanFactory 接口),不用 Bean 的话就用不了 Spring 框架,不用 Spring 框架的话…… 天哪,那你为什么还要在 Java 生态里受苦?除非你要开发 Minecraft Mod,那样的话,我表示敬佩。

为什么我讨厌 Java Bean 呢?封装性有什么坏处吗?在回答这个问题之前,需要反思封装是用来干什么的。

封装,简单来说,就是定义一系列数据以及可以作用在数据上的一系列操作。假设我们有一些零散的数据,姓名、年龄、身高、体重等等,把它们组合在一起,再定义改名、增长年龄、长高、变瘦等操作,把数据组合和对数据的操作放在一个叫作「人」的「容器」里,这就叫作封装。假设不把这些数据封装成类和对象,那么在操作数据的时候,就需要暴露很多细节。比方说,「人」的身高增长速度可能是年龄、性别等各种因素的函数,如果不封装的话,这个函数就暴露给程序的其他部分了,而其他部分大概率根本不需要关心这个细节。

封装的另一个意义是限制对数据的直接访问,比方说,如果「身高增长速度」不是函数而是状态,要是这个状态没有被封装隐藏起来,被外部代码修改过后,整个对象的数据完整性(integrity)就可能出错。

都是很合理的考量,但这和前面那几十行但逻辑含量为 0 的恐怖模板代码有什么关系?那些 getter/setter 方法根本没做任何检查,大部分时候都可以直接读写,和没有封装的区别在哪?强迫所有实体类都这样封装,仅仅是为了很小一部分的特例吗?为什么 Java 没有提供更好的封装方式?

我认为这就是设计缺陷,因为 CLOS 也用 getter/setter,写起来没有这么难受。准确来说,CLOS 使用的是 accessor,它既是 getter,也是 setter,并且和槽位(slot)一起定义。这个槽和成员变量类似。

(defclass person ()
 ((name
 :initarg :name
 :accessor person-name)
 (age
 :initform 0
 :accessor person-age)))

person 类定义了两个槽,nameage,每个槽都有一些选项,比如 :initarg:initform:accessor。这些选项不必要,对象定义可以写成 (defclass person () ((name) (age)))

:initform 接收一个形式,求值后作为槽的初始值。:initarg 接收一个关键词,在实例化类的时候作为参数使用,如下:

(defvar p1 
 (make-instance 'person :name "Tom"))

(person-name p1)
;; => "Tom"

(person-age p1)
;; => 0 (初始值)

如果没有默认初始值,也没有在 make-instance 时传入初始值,那槽的值就是未绑定的(unbound),试图访问就会报错。

你可能发现 :accessor 的作用了,它在没有声明函数或方法的情况下,仅仅通过指定符号名,就创建了 getter 方法。它同时也是 setter 方法,可以搭配 setf 使用。

(person-name p1)
;; => "Tom"

(setf (person-name p1) "Ben")
(person-name p1)
;; => "Ben"

这看起来只是 Java Bean 的简写版,但 CLOS 不强制封装,也就是说,即便没有 :accessor,也可以通过 slot-value 访问对象的槽。

(slot-value p1 'name)
;; => "Tom"

这在 Java 开发里是「破坏封装性」的做法,实际上,有相当多应用在 Java 中的设计模式都围绕「封装性」展开。Java 类的封装是神圣不可侵犯的。

比方说, 备忘录模式 (Memento Pattern)。

描述备忘录模式的 UML 类图和时序图。类图中有 Caretaker、Originator 和 Memento;Caretaker 访问 Originator,Originator 创建 Memento;Originator 和 Memento 都有 state 属性,Originator 有创建备忘录和通过备忘录恢复状态的方法,而 Memento 只包含相关的 getter/setter 方法。

图源: Wikipedia

这个设计模式把源发器(Originator)在某个时刻的状态保存在单独的备忘录(Memento)里,源发器可以创建备忘录,也可以用备忘录恢复状态。这种设计模式用来实现撤回和版本管理等操作。

之所以需要单独的备忘录类,而且必须让源发器自己创建备忘录,正是因为「不能破坏类的封装性」。源发器的属性是私有的,其他类不能访问,所以需要 createMemento() 方法来创建备忘录,其他类也不能给它的私有变量赋值,所以需要 restore() 方法。

值得注意的是,这里的 state 仅仅是简化描述,实际上源发器可能有许多内部状态,这些内部状态要被保存在备忘录类里的话,备忘录就要有一模一样的成员变量才行。这不仅涉及到重复代码,源发器和备忘录还是紧密耦合的。

所有这些复杂性的引入,仅仅是因为:我们不能破坏类的封装性。

To close, or to CLOS?

Java 的另一个语言限制是封闭类。「封装性不可侵犯」更多是生态和社区方面的共识,而封闭类则是实打实的语言限制。封闭类的意思是说,类的变量和方法,只能在类当中定义,并且类定义只能写在一个文件里。

我们还在某一个设计原则里见过「封闭」这个词,就是开闭原则(Open-Close Principle)——程序应该对修改封闭,对拓展开放。默认情况下,封闭类对拓展是封闭的,除非利用继承创建新的类;要增加类的行为(比方说添加新的方法),就必须修改类定义。

当然,围绕着这个限制,也有一个可怕的设计模式出现了—— 访问者模式 (Visitor Pattern)。

访问者模式的类图和时序图,其中定义了 Visitor 和 Element 接口,有 ElementA、ElementB 和 Visitor1 实现类,值得注意的是,Visitor 接口中包含了 visitElementA() 和 visitElementB() 方法。

教我设计模式的老师在讲解这个模式时不断称赞其优雅,可我一直皱眉头。注意看,Visitor 接口定义了两个方法,分别是 visitElementA()visitElementB()。你注意到问题在哪了吗?

整洁的架构 中,抽象的不应该依赖具体的,因为抽象的是稳定的,具体的是多变的,抽象类若是依赖具体的类,具体类的修改就会影响抽象类,使得系统中的修改变多,增加维护难度。显然,Visitor 作为抽象的接口,不应该依赖 Element 的具体实现。若是编写代码实现上述 UML 类图,Visitor 接口的定义中必然会包含 ElementAElementB 的源代码引用。

就算只让 Visitor 依赖同样抽象的 Element 接口,让 Visitor 的实现类去依赖具体的 ElementAElementB,这两个接口仍然是紧密耦合的,就和备忘录模式一样。就像源发器需要定义 createMemento()restore() 方法,被访问的元素类也需要 accept() 方法,来允许访问者访问其内部数据。如果不这么做,就要破坏封装性。

为什么被访问者自身不能提供访问数据的方法呢?比方说,toString()toJSON()toTOML() 等等。问题在于,在 Java 里,这违背了开闭原则。如果需求变更,需要添加将数据导出为 YAML 格式的能力,就不得不违背开闭原则,修改类定义,添加 toYAML() 方法。

为了让程序可拓展,尊重开闭原则,访问者模式就诞生了。因为不能在不修改类定义的情况下添加新的方法,所以就只能实现新的类来拓展功能。

如果类不是封闭的呢?如果可以在别的文件、别的模块里给类添加新的方法,那不就没必要创造与实体类紧密耦合的访问者类了吗?实际上 Go 语言就可以做到,详情可以阅读 这篇文章 ,不过 Go 语言因为缺少继承,不算是真正的 OOP 语言。

在 Common Lisp 中,由于方法不属于类,是独立存在的,再加上封装性不是强制的,可以用 slot-value 访问没有 accessor 的槽,所以导出数据的操作就不需要被访问类亲自动手,也不需要与一个访问者类耦合,提供必须的 accept 方法。

CLOS 中方法与类分离、不强制封装的设计,使得模块之间能保持松散的耦合,访问者与被访问之间不需要相互了解,数据完全可以不必得知访问者的存在,访问者无需复杂的操作就能直接获取对象的内部数据。这并不可怕,除非对数据安全有极其严苛的要求,然而这在大部分时候不适用。

封装和封闭不应该是不可变的祖宗之法。

继承是必要的吗?

说实话,不少 Java 设计模式自己都讨厌继承这一 OOP 的基本机制,更常用的模式是编写抽象接口,然后再编写接口的实现类。翻看四人帮的 23 个经典设计模式,几乎没有一个模式用到了继承。

因为在那本《设计模式》里,重要的原则之一就是: Composition over inheritance (组合优先于继承)——也就是 CRP(组合复用原则)。

一方面的原因是继承不够灵活,行为和结构是自上而下预先定义好的,所有数据和所有操作都绑定在一起,不利于复用其中的某些部分。比方说,假设有多种类型的账户,这些账户在计算价格时有不同的算法逻辑,比起创建 Account 类和各种类型的子类,更利于复用的模式是将算法和数据分离开来,创建 Strategy 接口和各种实现类,计算价格时由 Account 提供 Strategy 的实例,再调用其中的方法进行计算——这个叫 策略模式 (Strategy Pattern)。

另一方面,也是更重要的方面,是 Java 的另一个语言限制:不支持多继承。从某种程度上讲,策略模式的出现也是因为单继承的限制,组合复用更灵活;另一个原因是旧版本 Java 不支持函数式编程,而现在有了 Lambda 表达式也很少使用,因为 Lambda 不能用类定义。

既然在专门写给面向对象编程的书里,都出于复用性考虑,拒绝使用继承,那继承对 OOP 真的还必要吗?Go 语言支持 OOP,但并不支持继承(也因此有人认为 Go 不是面向对象编程语言),并且 Go 语言创始人 Rob Pike 本人也强调 用更小的接口实现更强的抽象

噢等等,Rob Pike 还在这场演讲中提到了 Java,我们来看看他怎么说的:

Now, if you come from Java, where everything is bigger, you think of interfaces typically having lots of methods.

如果你来自 Java(那里什么东西都更大),你印象中的接口通常有很多方法。

谁会不喜欢 Java 笑话呢?

使用继承时若是稍不注意,就容易违背里氏替换原则(LSP),比如经典的圆形/椭圆形问题(也叫正方形/矩形问题),指的是圆形和正方形作为椭圆形和矩形的子类,会违背里氏替换原则。具体的解释可以阅读 这篇文章

继承的目的是行为复用(behavior reuse),子类可以复用(或重写,或拓展)父类的行为。在经典的四人帮设计模式中,继承被基于接口的组合复用取代了,他们更倾向于把行为从类中分离出来。在一些动态语言中,类的概念被取消了,取而代之的是原型(prototype),可以现有的对象作为原型,复用其属性和行为,不必先有类才能实例化,这叫做 基于原型的编程 ,可以理解为没有类的面向对象。实际上 JavaScript 就支持原型编程。

看起来,继承难用到不少人都在找新的解决方案以避免和它打交道。

话说回 Common Lisp,这门语言的对象系统实际上支持继承,而且是多继承,子类可以组合复用多个父类的行为,这实际上也和四人帮的「组合优先于(单)继承」不谋而合,只要类足够小且内聚。多继承的语法也简单得要命。

(defclass baby (child person)
 ())
;; baby 类同时继承了 child 类和 person 类

要实现灵活的组合复用,可以选择多继承,也可以应用接口隔离原则(ISP),创建更小的接口。CLOS 里没有常规的接口,只有在文章一开始提到的 defgeneric 泛函数,再加上另一个语言特性,也能实现灵活的组合复用。这个特性我在 第 82 期周刊 提到过,让我从那篇文章里选个例子。这段代码来自 Artyom Bologov

(defmethod (setf url) :around (value (buffer document-buffer))
 (call-next-method)
 (set-window-title))

这段方法定义实际上覆盖了 (setf url) (但只会分发给以这个类的 url 槽为参数的方法),当 url 这个槽被更新(setf),就调用更新 UI 的函数。这有点像面向切面编程,在 Java 等 OOP 语言里,子类也经常对父类做这种操作。行为复用的目的经常是拓展原有的行为。

如果要在 Java 里实现「更新 URL 后也同时刷新 UI」,由于要避免继承,就只能求助于 装饰器模式观察者模式 了。

简单来说,行为复用不要求继承,更不是只有 Java 的单继承模式才能做到。甚者,经典的面向对象设计模式常常绕过继承,通过接口实现组合复用,而在 CLOS 里,多继承和 :around 等语言特性,使得行为复用变得更简单灵活。

设计模式,还是规避方案?

这个观点可能不新了,在 Peter Norvig 的 Design Patterns in Dynamic Languages (动态语言中的设计模式)演讲中,他提到在包括 Common Lisp 在内的多种动态语言里,23 中设计模式中的 16 种要么消失了,要么被简化——我得提醒读者,这些设计模式里还有 解释器模式 ,用于嵌入脚本语言,属于和架构关联较弱的模式,这是剩下的 7 种之一。

这个演讲第一次呈现是 1996 年五月,而刚好三十年过去了,这二十多种面向对象设计模式仍然被无数的 Java(以及其他传统 OOP 语言)开发者奉为圭臬。我想这并不是他们想不到这个观点,而是因为他们没有或者很少接触其他的语言。我也在和人打交道的过程中逐渐意识到,大部分求职者和从业者对编程并无热情,Java 也好,C++ 也好,都是他们求职的工具而已,编程语言对本身而言并不有趣,所以不会去探索更多的语言;或者,另一部分人更关注业务,成为了管理者,与技术保持着微妙的距离。

而在他们眼里,设计模式的确有帮助,甚至是「优雅」的(不得不说,「优雅」是非常主观的评价),因为这的确让编写软件变得方便了。只不过方便的源头,是本身就存在的语言限制,而设计模式仅仅是提供了这些限制的规避方案而已——这些限制在一开始可以不存在。

不过论题可能会被转移到语言的动态与静态上,动态语言的名声在我看来是两极分化的。吹捧者(包括我)认为动态语言限制很少,写起来令人舒适,语法也往往更简洁,需要维护的代码量更少;反对者可能认为动态语言容易出错,不少类型检查都发生在运行时,缺少编译时保护——我对此的反驳是,要保证软件质量和安全性, 写测试就好了 ;况且静态类型不代表类型安全,这是两个概念,同理,动态语言也可以做到类型安全。

话说到最后,究竟是选用灵活但效率和安全性可能更低的动态语言,还是在静态语言系统里用复杂的设计模式规避限制,都是开发者自己的权衡取舍。


Happy Lisping!


  1. Common Lisp Object System(Common Lisp 对象系统) ↩︎

Just A Common Lisper

2026年6月2日 17:53

最近在玩 Common Lisp ,由于中文博客几乎没有讨论,包括文档在内的英文资料都很零散——官网上只有简单的上手介绍,而社区维护的 文档 的某一页竟然写着 Please help us fill this page,我想这是因为 CL 的实现太多了,相关的网站很多,人力是分散的——看来得自力更生了,也借此写一篇博客,既用作信息整理,也试图提高一些 Lisp 的讨论度吧。

尽管 Common Lisp 的具体实现之间差异不会太大,但我想还是说明一下比较好:我使用的 Common Lisp 实现是 Steel Bank Common Lisp ,即 sbcl

文章没有什么主题,可以当作一篇 Lisp 杂谈,还有一些我对编程语言的思考,希望读者可以在文中找到一些有启发的观点和设计吧,不过我不保证什么。

Enter: Common Lisp (He says in parentheses)

我的启蒙 Lisp 是 Clojure ,这门语言最明显的特征是使用了 ()(Parentheses)以外的其他括号,比如 [](brackets) 和 {}(braces)。其中,[] 表示向量,也被大量地用在函数签名、let 绑定等语法上;{} 表示映射,放在花括号里的奇数个元素是键,偶数个元素是值。这么做的好处是极大地缩短了代码,坏处是…… 不够纯粹?

据我所知,Common Lisp 和 Scheme 都只有一种括号,语法非常简单,所有程序结构都用圆括号包裹,不需要额外记忆别的语法。几乎没有语法也是 Lisp 的特点之一,而 Clojure 的做法无疑引入了不少新语法。

我目前还不能很好地回答这个问题,我只知道,在 Common Lisp 里不能像 Clojure 那样爽写映射,很不习惯。

要在 Clojure 里表示键值对,可以这样写:

{:key1 "val1"
 :key2 "val2"
 :key3 "val3"}

要在 Common Lisp 里表示键值对,需要用到 cons,这是我在 Clojure 里没仔细了解过的概念,不过据说是 Lisp 的基础概念,在包括 Clojure 在内的大多数 Lisp 方言中都有实现。这被称作「列表构造函数」,(cons x y) 的意思是「把 x 加入 y」。cons 的意思应该是 construct(构造)。

两个参数都是原子的 cons 调用构造有序对(ordered pair),例如 (cons 1 2) 的值被表示为 (1 . 2),其中的 . 表示这不是列表,而是一对值,这种类型的值被称作 cons pair 或者 cons cell

一个有序对的前半部分被称作 car,后半部分被称作 cdr(car (cons x y)) 返回 x(cdr (cons x y)) 返回 y。这两个词的意思分别是寄存器编号的地址部分的内容(Contents of the Address part of the Register Number)和寄存器编号的减量部分的内容(Contents of the Decrement part of the Register Number)。之所以这么命名,貌似是出于一些历史原因,最早的 Lisp 是在上世纪的 IBM 704 计算机上实现的,和硬件强相关。不过具体的背景我就不打算深究了,这不是词源学系列。

当我们嵌套有序对,就构成了列表,例如 (cons 42 (cons 69 (cons 613 nil))) 构造以下的数据结构。最后一个 nil(空值)表示列表结束。

图源: cons - Wikipedia

一般而言,上述结构用 (list 42 69 613) 简单表示。

显然,有序对除了不断嵌套表示列表结构,其本身就可表示一个键值对,即用 cons 表示键和值的关系(association)。把一系列扁平的关系都放进列表里,就成了关系列表(association list),也就是 alist

(defparameter *alist* (list (cons 'a 1)
 (cons 'b 2)
 (cons 'c 3)))

这段示例代码来自 Marin Atanasov Nikolov ,其中 'a 表示对符号 a 的「引用」,以 ' 开头的符号不会被求值,而是会原样保留,可以用来表示键。

要获取 alist 当中的某个有序对,可以用 assoc 函数。(assoc 'a *alist*) 会返回 (A . 1)1。如果只想要键或者值,用 cdr 或者 car 就好了。此外,还可以用逆向关联 rassoc(reverse association),通过值来查找有序对,(rassoc 1 *alist*) 也会返回 (A . 1)

这个数据结构没有引入新的语言机制,非常简单。请思考一下,如果要给这个映射添加新的关系,应该怎么做?

cons 把新的对加入到原来的列表中就行了,就像这样:(cons (cons 'd 4) *alist*)。还有另一个很好用的函数叫作 acons,它可以从第一和第二个参数创建新的有序对,然后再执行 cons,这样就可以少一层嵌套了:(acons 'd 4 *alist*)。不过这两个函数都不会修改原来的 *alist*,而是返回新的数据结构,编写函数式程序很合适;如果想要改变原来的变量,可以用 push,就像这样:(push (cons 'd 4) *alist*)

此外还有一个和 Clojure 的 zipmap 很像的 pairlis 2,接收两个列表,然后按照他们的索引(index),把他们组合成 alist

(pairlis '(a b) '(1 2))
;; => ((B . 2) (A . 1))

有序对组成的列表看起来很符合直觉,不过从结构上看,((A . 1) (B . 2)) 为什么不能变成扁平的 (A 1 B 2) 呢?我联想到数据库设计的 Schema-On-Write 和 Schema-On-Read,前者坚持写入提前定义好的结构化数据,数据的结构本身就应该表达明确的语义,后者则在读数据时才解析数据结构(比如可以把 JSON 字符串写进数据表里,读取之后再解析 JSON),更加灵活也降低了数据库设计的复杂度。

Clojure 的 {} 语法就是这样的,Common Lisp 也可以做到,可以直接把键值对按照奇偶顺序写进列表里,这种列表叫作属性列表(Property List),即 plist。从结构上看,plist 只是列表,不能表示映射关系,但在读取数据时,可以按照约定俗成的方式解析,此时它就有了映射关系。用 getf 就可以从 plist 中获取键对应的值。

(let ((plist (list :a 1 :b 2)))
 (getf plist :a))
;; => 1

其中 : 开头的值表示关键词(keyword),和 Clojure 等 Lisp 方言一样,类似无需提前定义的枚举类型,一般表示键值对的键。尽管这样一来和 Clojure 的差别就小了些,但还是要写个 list 函数。

这么看来,Clojure 作为一门目前看来比 Common Lisp 抽象层次更高的语言,帮我隐藏了不少底层细节,用起来令人愉悦(比如 Clojure 中 car 对应 firstcdr 对应 rest)。Common Lisp 尽管在大多数时候也不需要接触这些细节,但在学习时会自然地接触到相关概念,体感上是更适合用来研究(满足好奇心)的语言。

Not all are functions

Lisp 的基石当然是列表,一般的列表都表示函数调用(至少看起来是这样),第一个列表元素是函数名,之后的元素都是参数。如果不关注语言底下是什么,这么理解 Lisp 代码没有问题,加法 (+ 1 2) 是函数,逻辑运算 (or t nil) 是函数,赋值 (setq *foo* "bar") 也是函数——语法统一,非常美好。

但事实并非如此,不是所有列表都是函数调用,有些东西无法通过函数实现。比方说 if,它接收三个参数:条件、条件为真时求值的表达式和条件为假时求值的表达式。

(if (= a 1)
 (format f "A equals 1")
 (format f "A does not equal 1"))

如果 if 是函数,那么这个函数是没有意义的,因为在调用函数时,程序会先求值所有的参数,然后把求值过后的参数传递给函数体。然而,if 存在的目的不就是不求值某一条分支语句吗?

况且 format 函数的返回值是 nil,上述代码如果是函数,只会得到这样的表达式:(if t nil nil)3——无论 if 函数怎么实现,这种调用都没有意义。

or 也不是函数。或运算只要有一个真值,整个表达式的值都为真,那么把所有参数都求值过后再计算就很低效,在求值到第一个真值时就可以返回 t 了。

这些不是函数的“函数”,要么是宏,要么有着特殊的解释(或编译)规则。

之所以想谈这个话题,是因为我在某个课程设计项目中嵌入了一门脚本语言。什么样的语言最容易编写解释器呢?当然是 Lisp。只需要根据括号和原子进行分词,根据词元构造出抽象语法树,再递归地求值就好了。(其实我也可以把 Lua 之类的轻量脚本语言嵌入进去,但为什么不用 Lisp?)

其中有相当一部分“函数”会直接绑定到某个 Go 语言函数上,另外一些被定义为「特殊形式」,会做特殊处理。不过我在这里做了一个不太明智的决策:由于这是个多维表格应用,所以我内置了一些快速获取表格数据的「特殊形式」,比如 (where this (= id 1)) 能够获取 id1 的行,其中 (= id 1) 创建了新的上下文,id 被绑定到表格的列名上,并且每次遍历都会执行一遍,这显然不是个函数,因为 (= id 1) 若是直接求值就会报错。如果要用函数实现,就要传入一个用于筛选的匿名函数,比如这样 (where this #(= (get % "id") 1))4——这更符合直觉,但语法又变得复杂了。

然而编写特例又会让语言本身变得臃肿,所以另一种做法是编写宏(macro),也就是能够写代码的代码。既然 (where this #(= (get % "id") 1)) 可以用函数实现,不必编写特例,而 (where this (= id 1)) 更简洁,那我能不能编写宏,让 (where* this (= id 1))5 在解释(或编译)之前就被展开为 (where this #(= (get % "id") 1)) 呢?

显然是可以的,或者至少可以做到类似的形式,并且宏定义是可以写在标准库和第三方库里的,这样一来,就无须在解释器(或编译器)中编写特例。

回到 Common Lisp,whenunless 宏会被展开为 (if ... do nil)(if ... nil do),而 Common Lisp(当然 Clojure 和不少 Lisp 方言都有这两个宏)的解释器和编译器不需要单独实现 whenless,只需要实现 if,剩下的只要交给宏就好了。

当然,除了保持语言简洁,宏还能方便程序员拓展语言能力,用自己喜欢的方式编写代码而不必忍受语言限制。

Not all functional

可能是因为我一开始读过的不少 Lisp 相关文章和书籍,都把 Lisp 和函数式编程联系起来,我一开始以为 Lisp 都是纯函数式的(思来想去,应该是鲍勃大叔的《整洁架构之道》把函数式编程范式称作「不少 Lisp 方言实现的范式」,误解就由此产生了),我还因为 Haskell 是纯函数式的编程语言,把它误以为是 Lisp 方言,闹过一些笑话。

Clojure 的确是一门整体上更偏好函数式编程的语言(关于什么是函数式,可以读 这篇文章 ),比较直观的体现是,几乎所有的 Clojure 教程和示例代码都没有定义变量然后再赋值的教程。默认情况下,几乎所有变量都是不可变的,包括用 let 局部绑定的变量。

而 Common Lisp 似乎不强调这点,Common Lisp 可以轻易地给变量赋值,不过这种变量被称作动态变量(dynamic variable)。

(defparameter *dynvar* 1)
(setq *dynvar* 2)
*dynvar*
;; => 2

不过我还是更偏好无状态的程序,就算要用全局动态变量,也应该控制数量。只要不会太繁琐,用参数传递状态就足够了。如果要定义局部变量,就用 let

(let ((a 1)
 (b 2))
 (+ a b))
;; => 3

let 显然不是函数,而是特殊形式,因为它创建了一个变量作用域,还包含未声明的符号,这些符号若是被当作函数参数求值就会报错。不过,它底下到底是怎么实现的呢?和其他语言的 let 变量声明一样吗?

在《Let Over Lambda》一书中,作者这样写道:

Although what let does is unambiguous, how it does it is deliberately left unspecified. What let does is separated from how it does it. Somehow, let needs to provide a data structure for storing pointers to values.

尽管 let 的作用并不模糊,它发挥作用的方式却被刻意地留白了。let 的作用与它的作用方式分离。通过某种方式,let 需要提供用来储存指向值的指针的数据结构。

据后文所言,Common Lisp 编译器会根据情况处理 let,选取最好的「存储」方案。程序员也可以在代码中提供更多的信息,这样 Lisp 编译器就能编译出效率更好的代码。

(defun register-allocated-fixnum ()
 (declare (optimize (speed 3) (safety 0)))
 (let ((acc 0))
 (loop for i from 1 to 100 do
 (incf (the fixnum acc)
 (the fixnum i)))
 acc))

这段代码中的 declare 就是给编译器的优化「建议」,编译时,这段函数会把 let 里的数据加载到 CPU 寄存器中,效率会很高。

书中还提到了 lambda(毕竟书名就是《Let Over Lambda》,书名其实是书中一种重要的 Lisp 编程模式,也缩写成 LOL),这里的 lambda 指代的是 λ 演算(λ-calculus),现代的非 Lisp 编程语言也把这个特性拿取用了,成了所有程序员都熟悉的「匿名函数」。

书中提到,let 的一种实现方式就是 lambda,但并没有展开,此时我们可以把目标转向《计算机程序的构造与解释》(SICP)一书。

The first part of the let expression is a list of name-expression pairs. When the let is evaluated, each name is associated with the value of the corresponding expression. The body of the let is evaluated with these names bound as local variables. The way this happens is that the let expression is interpreted as an alternate syntax for

((lambda (<var1> ...<varn>)
 <body>)
<exp1>
...
<expn>)

No new mechanism is required in the interpreter in order to provide local variables. A let expression is simply syntactic sugar for the underlying lambda application.

这段代码首先使用 lambda 创造了一个匿名函数,这个函数有 n 个形参,同时,这段代码直接调用了这个函数,传入了 n 个参数。对 Lisp 不熟悉的读者(真的会有那样的读者读到这里吗?)可以参考 JavaScript 的立即执行函数。

(function(a, b) {
 return a + b
})(1, 2);
// => 3

这相当于:

((lambda (a b) (+ a b)) 1 2)

;; 相当于
(let ((a 1) (b 2))
 (+ a b))

要注意的是,SICP 使用的是 Scheme 而非 Common Lisp,不过你也看到了,这两门语言其实有不少相似之处。区别之一当然就是,Common Lisp 不指定 let 的实现方式,编译器可以做各种优化;而 Scheme 中,let 就是 lambda 的语法糖。

后者当然是更函数式的——有什么比创建一个立即执行的匿名函数以实现局部变量更函数式的?从效率方面考虑的话,调用函数就要在内存中创建函数栈帧,而经过优化的 Common Lisp 程序可以把数据直接存放在 CPU 寄存器里,这在执行效率上是更好的。

此外,Common Lisp 还能够实现 OOP——用 defgenericdefmethod 实现多态,用 defclass 定义类,用 make-instance 实例化类——尽管函数式和面向对象并不冲突,但这也说明 Common Lisp 不那么注重函数式风格,它更像是一门什么都能干的语言。


后面的知识,之后再来探索吧。


  1. Common Lisp 大小写敏感,所有代码最终都会被转换为大写字母。 ↩︎

  2. 我没拼错,就是 pairlis,没有 t ↩︎

  3. Common Lisp 用 t 表示逻辑真,用 nil 表示空和逻辑假,没有 f 或者 false。 ↩︎

  4. 这里借用了 Clojure 的语法,其中 #() 表示匿名函数,% 指代函数的参数。值得一提的是,Common Lisp 中的 #() 表示数组。 ↩︎

  5. 这里之所以写的是 where* 而非 where,是因为宏和函数共享命名空间,不可能有一样的名字。 ↩︎

稻草人周刊 Vol.83

2026年6月1日 07:39

我感到自己最近一直处于应激状态,随时准备和世界反目成仇、大干一场,这种状态大概是不适合做任何人生决策的,我在想应该调整调整心态,或许暑假该出去走走,也暂时把找实习的事情放一放。写作也有帮助,至少我在写词源学系列的时候心无旁骛,感到开心。


止语

the cure music cover

the cure

Olivia Rodrigo

But my head is full of poison, and my heart is full of doubt

I got toxins in my bloodstream, you tried hard to suck ’em out

And it feels like medication, and it’s good for me, I’m sure

But it don’t matter how your love feels anymore

It’ll never be the cure


连接

系统之乏与特异之悦

📜

Marcin Wichary 注意到了 1997 年的软件 ClarisWorks 的提示框设计,按钮上写的不是「过会儿再说」「现在注册」「永不注册」,而是简单的十三个字母 Never, Later, Now(永不,稍后,立刻)。这看起来违反了 Avoid “Click Here” 规则(即按钮不应该使用「点这里」等模糊的词),用户如果不读提示框上的文字,就不知道按钮是用来做什么的。

ClarisWorks 的提示框

改成 Never RegisterRegister LaterRegister Now 能解决这个问题,但按钮看起来就不会这么整洁了。在我看来,丢掉多余的 Register 也能避免语义冗余,字太多不但会造成视觉负担,还会造成理解负担。

Jim Nielsen 对此有些思考,他把 Avoid “Click Here” 规则定义为「系统规则」,把 ClarisWorks 看似违反规则的做法当作最适合当前软件的「本地化方案」。他感叹,如今的软件为了扩大规模,貌似已经完全用成熟的「系统规则」代替「本地化方案了」,因为前者又快又足够好,费心思设计提示框的文字不仅费时费力,还不能带来显著的回报。

As software moves towards “scale”, I can’t help but think that systematic rules swallow all decision making because localized exceptions become points of friction — “We can’t require an experienced human give thought and care to the design of every single dialog box.”

我对此的看法是,在软件行业内的商业软件兴许不必在乎特异的、令人愉悦的本地化方案,只要跟着已经工程化的、被证实有效的经验走,就能快速且稳定地交付成果。不过,真正改变人们对软件看法的软件,往往不来自商业世界,比如 Linux;就算来自商业世界,也来自那些不守规则的人,比如 Notion 和 Arc。改变世界的东西,在我看来,很少来自追名逐利、总是试图复刻成功的人,而来自那些抱有热爱,匠心胜过野心的爱好者。

不过这两类人也不是割裂的群体,毕竟爱好者也要吃饭。

不推荐 Bitwarden

📜

文章列举了密码管理器 Bitwarden 的一系列缺点。这是个商业软件,提供订阅制的服务,同时也有自托管选项,但作者表示 Bitwarden 的官方后端完全是给企业使用的,普通人部署的成本很高。当然,有社区用 Rust 实现的 Vaultwarden 作为替代后端,个人用户自托管的成本被降低了很多,不过即便是这样也要依赖 Bitwarden 的客户端(尽管是开源的,也可以 Fork)。

之后作者还列举了 Bitwarden 的设计在用户体验上的问题(由于 Vaultwarden 是实现了 Bitwarden 的 API,所以这些体验问题我想也无法在这个开源的替代品中修复),比如在密码库之间移动项目,必须先把要移动的项目以明文导出,然后再导入要迁移至的密码库中,不仅麻烦而且很不安全。有些时候 Bitwarden 客户端更新会弄坏一些东西,还有很多烦人的小细节。此外,作者还列举了从 2023 年到今年,Bitwarden 的各种安全性问题,比如前些日子的 NPM 供应链攻击就波及了 Bitwarden 的 CLI。对密码管理器来说,安全问题是致命的。

我想补充的是,不久前 Bitwarden CEO 换人,引起了不少 风波 。尽管目前看起来安好,但貌似已经有一些发疯的前兆了。我决定脱离 Bitwarden 生态。

我是前不久才自托管 Vaultwarden 的,当时只是想要快点找到一个 Apple Passwords 的替代。一方面是为了完全自己掌控数据,另一方面是长远考虑,我之后可能会半脚踏出苹果生态,去整一台 Linux PC(不过近期还不打算折腾)。Docker 镜像跑起来的时候我就心生疑惑:这东西跑在公网上真的安全吗?密码库有必要二十四小时在线吗?

答案是没有必要。再加上 Bitwarden 最近的事情,我决定完全迁移到 KeePass 生态。KeePass 用单个加密的 .kdbx 文件作为密码库,不需要庞大的数据库和 Web 服务,用户可以把密码库放在任何位置,只要能同步文件就能够同步密码库。密码库主要用主密码解锁,还可以附加密钥文件和 Yubikey 做额外的保险措施(可惜我的 Canokey 貌似不支持相关协议,我没有深入研究)。我使用的客户端是 KeePassXC ,iOS 使用的是 KeePassium ,目前暂时通过 iCloud 同步密码库。

密码库上锁之后,浏览器插件就没办法和 KeePassXC 通信,解锁需要打开 KeePassXC 而不能在浏览器里完成,所以体验上会差一些,但还算能忍受。迁移之后我使用了大概五天,目前基本满意。

前端极大化主义

📜

一种架构设计方案:如果有一些大小适中的数据需要发送给前端进行展示,过滤和排序等操作不必在后端完成,可以把完整数据直接发给前端,由前端处理;这部分数据还能缓存在前端,需要重新排序或过滤的时候不必向后端发送请求。

当然,这不适用于所有情况,只有数据量不会造成很大负担的时候才这样做。这不仅不会让网络连接不好的用户感到卡顿,反而会因为 HTTP 请求的减少而优化这部分用户的体验。很多人觉得发送所有数据不利于网络连接,但很多时候需要发送的数据甚至不如一张 JPEG 图片大。另一个理由是:现在没有那么多数据,但以后可能就有了。这和抽象层一样,很多时候人们编写了太多额外的抽象,但很久之后会发现,其实根本就用不到。

作者还引用了加尔法则(Gall’s Law):

A complex system that works is invariably found to have evolved from a simple system that worked.

A complex system designed from scratch never works and cannot be patched up to make it work.

You have to start over with a working simple system.

大概是说,不能一开始就把系统设计得复杂,应该从最简单的可用系统开始做。

简单来说,作者的观点是,不要仅仅出于习惯划分前后端的职责,前端除了展示,也可以做基本的数据处理。哪怕不用前端极大化的方案,仅仅思考「数据处理可能不需要全部放在后端」这件事就能帮开发者设计出更好的系统。

用 PNPM 规避供应链攻击

📃

我已经在 先前的一篇文章 中详细地报过菜名,列出了近期的 NPM 供应链投毒事件,结果仅仅过了一个月, TanStack 就被投毒了。JavaScript 的包管理生态极度中心化,非常容易成为攻击者的众矢之的,而 NPM 本身的安全措施显然不够有效。如果暂时没办法离开混乱的 Node.js 生态(我们能想象一个没有 Node.js 的前端开发未来吗?),采取措施规避供应链攻击就很重要。

首先,大部分供应链攻击都利用 postinstall,安装依赖后直接在开发者的电脑上执行代码,偷取储存在本地的密钥等关键信息,那么默认禁用 postinstall 脚本执行就能避免这部分攻击。PNPM 默认禁用 postinstall,需要显式地设置 allowBuilds 允许指定的依赖执行 postinstall 脚本。

第二个规避手段也是 PNPM 默认开启的, blockExoticSubdeps 会影响子依赖的安装(也就是 package.json 中没有直接声明的依赖,是依赖的依赖) ,如果子依赖来自远程 Git 仓库或 Tarball 直链,就会默认阻拦,只允许来自受信的目录(registry)、部分受信的 GitHub 仓库和本地路径。这个手段的有效性我持怀疑态度,恶意软件也可能来自受信的目录吧,默认屏蔽所有远程 Git 仓库和 Tarball 直链不也是在让整个 JavaScript 生态变得更加中心化吗?一旦 NPM 这个中心出问题,整个生态都会出问题,而 NPM 的安全性大家有目共睹。

第三个规避手段在我看来是最有效的:推迟依赖更新,因为大部分供应链攻击都会在几小时内被发现和修复。v11 版本的 PNPM 默认推迟一天,一天内发布新版本的 NPM 包不会被 PNPM 解析。用户可以把 minimumReleaseAge 设置项改得更长一些。

第四个手段是 PNPM 的信任策略,默认关闭,需要手动开启。 trustPolicy 目前只有一种策略,即 no-downgrade。开启后,如果某个包的信任等级在更新之后下降了,更新就会失败。信任等级是通过信任证据(trust evidence),我想应该是发布者的签名之类的,文档里没有写得很清楚。比方说先前的版本中,作者有给包签名,在某次更新中没有签名,那么信任等级就下降了,此时更新就会失败。

最后,无论如何都应该使用锁文件(lockfile),锁定依赖的版本号,并且要把锁文件 commit 到 Git 仓库当中。


星群

ggc

抱歉,我老是把名字看成某个 C 语言编译器

用 Go 语言写的 Git 命令行工具,可以当作 Git 的简单封装层,可以交互式地执行各种 Git 命令,还能全局模糊搜索命令。许多 Git 命令可以直接用 ggc 命令替代,而且变得更友好,比如 git commit -m "" 变成了 ggc commit ""(我一直觉得 -m flag 很烦),ggc add interactive 可以交互式地选择 diff(而不是整个文件)添加到 Stage。

整体而言,ggc 比 lazygit 更接近 Git,抽象层次没有那么高,而且更像 CLI 而不是 TUI。至于怎么选就看自己的偏好了。

访问: bmf-san/ggc


切片

  • 这周玩了很多的《饥荒》和《星露谷物语》,每天不是在建家砍怪,就是在一边钓鱼,一边等谢恩下班给他送礼物——这个男人怎么这么有魅力?

    你没活干吗?1

    我大概真的该好好干活儿了吧。

  • 把《 当人们不再问好 》发在联邦宇宙上之后,被转发了几十次,貌似又小小地火了一把,收获了不少关注。和以往文章不同的是,转发到联邦宇宙上时我附加了一段 TL;DR(太长不看版),兴许我以后都应该写写这玩意儿?

  • 周天去书店写东西的时候,看见坐在旁边的女生包上有一根黄瓜瑞克。

    另外《瑞克与莫蒂》第九季第一集真好看啊。

  • 继 Clojure 之后学的第二门 Lisp 方言是 Common Lisp(至于 Scheme,我也说不清楚为什么,总之暂时搁置了),打算用来写个小小的 Web 应用练手。CL 的各种设计我都还挺喜欢的,比如没有 false 只有 nil,调试程序时遇到报错可以直接替换有问题的值,在不修改程序的情况下临时执行,这样可以忽略当下不应该关注的代码细节。总之体验还不错。

    另外它能直接编译成二进制文件,而不是像 Clojure 那样编译成 JVM 字节码。据说用 Common Lisp 编写的 woo 是所有编程语言编写的 Web 服务器中速度最快的,还不知道具体体验如何。

  • 好久没跑步了,心率和呼吸倒是控制得非常好,不过心肺还没感受到负担,脚踝就在 1.5 公里之后不堪重负了,痛得不行。

    于是跑完之后又回来做了 10 分钟 HIIT 把强度拉上来。

    这么想起来人的脚踝真的承受了很多不该承受的重量呢。

  • 词源学小知识:意大利语 ciao 的本意是「我是你的奴」。


  1. 这是谢恩的台词 ↩︎

当人们不再问好

2026年5月30日 18:12

去年买了本法语童书作为阅读材料(后来发现即便是儿童读物,对我来说也有些难度,便闲置了),主角的爸爸在打电话时说了 allô 这个词。当时我以为是什么时髦的打招呼俚语,跟英语里的 sup(What’s sup?)一样,但查阅后才得知这是专门的电话用语,面对面打招呼时不说 allô

正巧,最近在重温《生活大爆炸》的时候,听 Sheldon 说电话的发明者 Alexander Graham Bell(后文称作贝尔)希望人们在打电话的时候用 Ahoy! 打招呼。

据我所知,如今英语使用者更倾向于说 Hello,并没有为打电话这个场景选择单独的打招呼用词。而汉语使用者倒是很少在电话以外的场景用「喂」打招呼,就算有,语调也完全不同。电话上的「喂?」带有疑问语气,是上扬的,用于询问和测试连线是否正常,而面对面交流的「喂!」则是下沉的,一般是为了引起注意而使用。这么说来,汉语也有专门的电话打招呼用语。

我闻到了《猫头鹰化石》的味道。

Ho, the ship ahoy!

Ahoy! 并不是贝尔生造的词,如果你看过一些海盗电影,或者在一些影视作品里注意过水手们说的话(抑或是某些角色戏仿水手或海盗时的用语),可能会熟悉这个词。Minecraft 的语言设置里有「海盗英语」这个选项,这其实是英语世界里非常具有传播度的文化现象,被称作「Pirate Speak」,甚至有一个非正式的节日名叫「国际像海盗一样说话日」,在每年的九月十九日。1

LingoJam 制作的 Pirate Speak 翻译器

其中 Ahoy 就是海盗英语的 Hello,不过海盗究竟是不是真的这么打招呼,就另当别论了。另外一个比较著名的海盗英语是 Aye,意思是 Yes——另一种说法是,AyeI 的变体,表示「我(同意)」2。《海绵宝宝》的片头曲就有 Aye, aye, captain!I can’t hear you! 的对白。不过 Aye 究竟是不是现实中海盗的专属黑话,也要另当别论,据我所知苏格兰人就更喜欢说 Aye 而不是 Yes3

尽管如今看来 Ahoy 更像是好玩的说法,有点像汉语里的 Oi!(光是想到这个词的发音我就要脚趾扣地了),不过贝尔生在十九世纪,那个时候海盗还没有作为热门的文化现象出现在公共视野,贝尔是不可能出于「像海盗一样在电话里打招呼会很好玩」这种理由提出 Ahoy 作为标准电话用语的——不过这么想还挺可爱。

要搞明白 Ahoy 是什么,还要往更久以前考查。

构词上看,Ahoyahoy 的组合,后者据说是中古英语的一种叫喊声。这不难理解,读音近似于 hoy 的叫喊声在各种语言里都很常见,类似的还有 ho(联想圣诞老人的「Ho! Ho! Ho!」)。我接下来又要举一个奇怪的例子了,在《饥荒联机版》中,要把桅杆上的船帆收起来,需要点击桅杆,此时的动作提示词是「Heave!」,然后角色会操作绳索收帆,等到文字变成「Ho!」之后再点一次桅杆,船帆就会快速收紧;如果在「Heave!」变成「Ho!」之前点击第二次,角色就会显得手忙脚乱,收帆的速度会变慢。我自己玩的时候,喜欢一边收帆,一边默念「Heave——Ho!」,只要节奏对了就能很快将船帆收起来,我想操作时机不对导致操作变慢的设定,可能也是在模拟航海时的「节奏感」吧。

「Heave-Ho!」虽然不是那么著名的海盗英语,但它的确是水手的黑话4,用在拉东西的时候,有点像汉语里的「嘿咻——嘿咻——」。Heave 的本意是「拉、提、举」,Ho 则是纯粹的语气词,所以更好的翻译可能是「拉!——嘿!」。

貌似大部分语言都有使劲时使用的语气词,我这边的方言有发音似「嘿作」的语气词。这些词的共同点是以喉咙音 H 开头,后接一个大元音,比如 ahohay

这可能是人类的生理结构导致的,即便不考虑文化背景,人貌似也会在搬运和托举重物、感到力竭时,不自觉地从喉咙发出声音。

回到 Ahoy 上来,hoy 本身是跨越多个文化的、自然的语气词,人类可能天然偏好用喉咙发出大气流音,即便没有语言也是如此。hoy 这种叫喊声本身是用于引起注意的,而 A 这个附加的元音可能是为了进一步引起注意。5

之所以不用简单的 HiHello,是因为 Ahoy 方便在距离较远的情况下引起注意(也就是…… 吼得比较大声?)。虽然不清楚海盗是否用这个词,但据 Wiktionary 和 Etymonline 所述,这的确是水手用语,完整的用法是:

Ho, the ship ahoy!

使用场景是这样的:如果水手看见附近有另一艘船,要让对方表明身份,就站在自己的船上大喊「Ho, the ship ahoy!」。如果有一艘小船(boat)正在靠近大船(ship),大船上的人就可以喊 Ahoy,小船的标准回应方式是:

  • aye aye 表示小船上有官员
  • no no 表示小船上没有官员
  • 回复大船的名字,表示另一艘大船的船长在小船上
  • 挥旗,表示一位海军上将在小船上

AhoyAye 这些看起来怪怪的词,可能都是航海时距离太远,需要大喊才产生的。

Ahoy! or Hello?

所以为什么贝尔希望用 Ahoy! 作为标准的电话招呼用语,而为什么 Hello 代替了它呢?

当我写下上面这句话的时候,我天真的以为自己能找到答案。实际上,相关的词源资料很少,难以考证,有不少自称「历史事实」的网站根本没有给出任何令人信服的来源和证据,完全是在凭空讲故事。而且我发现不少来源的说法都有细微矛盾之处,比如,大部分人都说 Ahoy 是贝尔提出的标准电话用语,而又有 说法 指出贝尔用的词实际上是 Ahoy-hoy

有趣的是,《辛普森一家》中的伯恩斯先生接听电话时说的是 Ahoy-hoy ,兴许是为了调侃他年事已高,说话方式也过时了。

电话刚诞生的年代大概是十九世纪早期,那个时候 Hello 也是个新词,人们并不是直接把面对面交谈的习惯迁移到了电话交谈上。实际上,电话对当时的人来说是非常陌生的发明,人们不知道如何使用。据说有一些顽固的人对新技术接受能力低,把电话称作恶魔的发明,使用者会被雷电击中,另外还有人觉得用电话线电击自己可以治好风湿病6——人类嘛,干出这种事不稀奇了。

而那些接受新技术的人,自然需要被指导如何使用。当时人们还会用电话簿(The Phone Book),通常是某个电话公司单独印刷和发行的,上面列出了所有客户(subscribers)的名字和电话号码——我想那时不同电话公司的电话没办法相互接通?这在现在看来简直是隐私噩梦啊。

电话簿上除了因为用的人不多所以也没有很长的电话目录,还有一些简短的指导,其中就包括打电话时应该先说什么、怎么打招呼。据说爱迪生(没错就是发明白炽灯的那个,不过他实际上只是对现有的灯泡做了改良)提出用 Hello 作为电话的打招呼用语,而这个词被印到了一些电话簿上7,至于贝尔的 Ahoy,出于某些我也找不到相关资料的原因,被爱迪生的 Hello 取代了,可能是某种自然选择吧。

顺带一提,据说第一本发行的电话册,建议人们用一声“ 坚定且欢快的 hulloa ”开始对话——我不知道这个词标准的发音是什么,但我总想把它读成「葫芦娃」。这个用词也没有保留下来。

我在 Internet Archive 上翻了半天,都没有找到这本「第一本电话册」,倒是找到了一本 1913 年的 Mansfield Telephone Company Directory,其中的确包含了电话用语指导。有趣的是,大概跟现代人厌烦了发消息先发「在吗?」一样,这本书指导用户不要说 Hello,而应该直接表明你的姓名。

Answer your Telephone Promptly: Answer promptly when you are called, giving your phone name or business instead of saying “hello.” This is essential to good service.

来自 1913 Mansfield Telephone Company Directory

据说贝尔的余生一直坚持使用 Ahoy 打招呼,也不知道他有没有原谅爱迪生。总之,Hello 就在电话公司的推广下成为了事实标准。

Hello? Why are you here?

前文提到,在十九世纪早期,Hello 并不是日常的招呼用语。实际上,hello 一开始就是作为电话用语出现在公众面前的,后来才变成了 hi 的同义词。实际上是电话用语入侵了日常用语,而不是反过来。

那爱迪生是怎么想到用 Hello 这个先前并没有被大众接受的词作为标准电话用语的?

据说在 1877 年八月 15 日8,爱迪生给匹兹堡中央区印刷电报公司(Central District and Printing Telegraph Company of Pittsburgh)的主席写了一封信,提出用 Hello 作为标准的电话招呼语,信件的内容是这样的:

Friend David,

I don’t think we shall need a call bell as Hello! can be heard 10 to 20 feet away. What do you think?

EDISON

—— THE GREAT `HELLO’ MYSTERY IS SOLVED, Roanoke Times

来源是 1992 的一张报纸,里面提到一个名为 Allen Koenigsberg 的学者用了五年时间,在美国电话电报公司的档案管理找到了爱迪生的信件。至于报纸是否属实,我也无从考证了,我无论如何也找不到这封信的原文。

当时的工程师们在思考如何让拨打和接听电话的双方知道电话已经接通了,爱迪生表示,说一句 Hello! 就好,不需要单独的铃声(call bell)。

所以…… 你还是…… 没有告诉我…… Hello 这个词怎么来的…… (颤抖并发出持续的水壶烧开声)

尽管 Hello 并不是打招呼用语,但据前人考究,在当时它的确是偶尔会被使用的用语引起注意的语气词,其来源…… 怎么又和航行有关?

有人认为 Hello 可能是受到了古撒克逊语 Halo! 的影响,这个词是 halōn 的不定式,意思是「叫;唤;拿取」,用于招呼船夫(ferryman)。不过更直接的关联是古英语里的 ēalā,其变体是 hēlā,更近代的形式是 hallo,后来演变为了 hollahollo,最后变成了爱迪生口中的 hello9

写到这里,我严重怀疑是爱迪生用过但不会拼 holla 这个词,才写成了 hello

hollahollo 最开始被用于引起注意,就像古撒克逊语里招呼船夫用的 halo! 一样。人们见面时打招呼会说 Good day!How do you do? 或者 How are you,不会说 hello。就像法语里的招呼语 Bonjour 其实也是 Good day 的意思,「嗨」「哈喽」本身就是没有实义的“声响”。

顺带一提,还有一个偏古典文雅的招呼语叫作 Hail,这个词也可以作为动词表示招呼某人。前不久很火的《挽救计划》的英文原名是 Project Hail Mary,其中 Hail Mary 指的是向圣母马利亚祈祷的经文。还有 all hail(全体致敬)这个词,表示更热烈的问号和欢迎。

出于某些原因,Hail 还表示「下冰雹」,但再细究下去这篇文章就写不完了。

Allô

古时候向来是英语向法语借词,justicegovernmentliberty 等概念无一例外都是法语词,更详细的内容我已经在《 为什么说英语是语言界的最大缝合怪? 》讨论过了。如今法语也在不断从英语借词,尤其是 IT 词汇,像购物网站的愿望单一词就直接使用了英语 wishlist 而非 liste de souhaits

然而,当我在思考法语的 allô 究竟是个什么东西的时候,我怎么也没想到——它就是 Hello 的法语写法啊!法语的 H 大多时候都是哑音。

Well, that’s disappointing.

不过 allô 并不是从 hello 演变过来的,而是 hallo,这更加让我坚信爱迪生是把词拼错了才造出 hello 这个词的

一般情况下,allô 不会在日常交流中使用,但在魁北克(加拿大法语区)、新英格兰、路易斯安那州和密苏里州,allô 会被用于日常打招呼。10借着法语这个桥梁,hello 这个词进入了其他欧洲语言,像西班牙语的 aló、土耳其语的 alo、巴斯克语的 alo……

仔细想想的话,这个词一开始只是一个有些粗俗,甚至不太礼貌的,用于引起他人注意的“声响”,是爱迪生这个发明家干涉了语言演变,hello 才通过电话用语进入到了日常用语,然后从英语再到法语,最后散播到包括中文在内的各国语言,很大程度上代替了原有语言文化中的招呼语。

「你好」「Good day」「Bonjour」甚至「What’s up」,都是从语义角度来看更好的招呼语,hello 作为毫无意义的简单“声响”,竟然能病毒式地闯入各种文化,真是神奇。或许缺乏意义的文字才更容易传播。

人肉装饰器

2026年5月28日 12:53

今年三月底,有一篇题为《 Meat-based LLM proxies 》的文章在 Hacker News 上小火了一阵,作者是这样描述某一类人的:

They’ll talk to you as if they’re human, except all of their words are written by an LLM. Anything you tell them, they feed to the same LLM and send you the response.
他们与你交谈,就好像真人一样,不过他们的话全是 LLM 写的。你告诉他们的任何话,他们喂给同一个 LLM,再把回复发给你。

Effectively, you end up talking to the LLM via a meat proxy.
实际上,你是在通过人肉代理跟 LLM 说话。

最近我也观察到了类似的现象,不过略有不同,我注意到的一类人并不会将我的话(或别人的话)复制粘贴到 LLM 对话框中,然后一字不差地把 LLM 的回复搬运过来,但他们看起来就像是高度依赖 LLM 一样,说话方式与 LLM 恐怖地相似。

我把这类人称作:人肉装饰器(Meat Decorators)。

四人帮的回响

面向对象设计模式中有代理模式(Proxy Pattern)和装饰器模式(Decorator Pattern),两者很相似,但用于解决不同的问题。

代理模式用于限制对被代理对象的直接访问,就好像一些房屋中介机构提供的「托管」服务,在房屋租赁的整个过程中,租客都不需要和房东打交道,签订合同、沟通维修问题和商议租金等等,都直接通过中介完成,不过最终都会汇总到房东。这可以降低软件内部通信的成本和整个系统的复杂度。

装饰器模式用于给某个类添加新的功能,覆盖部分行为。和代理模式一样,它也是对一个已有类的包装,不过他并不是为了限制访问而设计的。我举个稍微没有那么通俗的例子,我最近在写的项目除了要提供存储,还需要将第三方数据转换为格式相同的本地数据,理想情况下应该直接从外部数据源写入本地存储,但这实际上是两种职责,应当分离。于是我写了 Storage 接口处理本地存储,用 Adapter 接口转换数据,最后用装饰器类 AdapterStorage 把他们组合起来,这样就可以直接从 AdapterStorage 将外部数据写入 Storage 了(顺带一提 AdapterStorage 还是 Storage 接口的实现类,其他组件只需要依赖 Storage,根本不需要知道 Adapter 的存在)。

在人肉代理和人肉装饰器的比喻中,人类就是对 LLM 的代理类或装饰器类,前者让其他人类在不知情的情况下访问了 LLM 的功能,而后者用自己的人类能力拓展了 LLM 的能力,但拓展之后,其他人仍然在忍受着怪异的恐怖谷效应的情况下访问 LLM。如果真的要“拥抱‘AI’”的话,不应该让 LLM 拓展人类的能力吗?

装饰器的自由意志

不,我说的不是「喜欢用破折号」这种容易被误以为是 LLM 的例子——我就喜欢用破折号,仔细观察的话你会发现大多数 LLM 使用破折号的方式和人使用破折号的方式完全不同。我说的也不是那些在对话中无法提供见解,就说「我跟“AI”对话,它说……」「可是“AI”说……」的人。我说的是一种更难归纳总结的,机器人般的人类行为。

其中明显的特征是,和人肉装饰器讲话时,我感受不到作为人的真诚。真诚这个词可能不准确,我想不到更好的了,这里说的并不是「没有欺骗」和「坦诚相待」,而是说,能从话里行间感受到这个人正在用他的经验、想法、思考问题的方式和独特的语言风格回复我,回复我以相同的方式组织的话语。

人肉装饰器有自由意志,他们知道自己要干什么,但他们不动用自己的认知能力去做这些事情,而是外包给 LLM。最容易辨别的特征是他们的语言风格,因为 语言是思考的边界 ,而他们几乎把大部分的思考、求知和探索都外包给了 LLM。

比方说,在解决某个问题后复盘原因时, 小氯 可能会以「老友们」开头娓娓道来,然后引用某个欧洲小国的历史事件做比喻,而人肉装饰器会这样写:

啊,这下就有意思多了,这说明问题不在于工具本身,而在于“拥有工具,却不知道正确的使用方式”

  • 有的人会因为掌握了工具就把自己当专家
  • 有的人对工具避而远之
  • 有的人使用了工具之后,还保留原先的思维方式
  • 有的人把工具当作自己的“主人”

最后你会发现,工具使用本质上像是“主仆社会学实验”

我本来还想加上 Emoji,但那样就有点太讨厌了。请读者自行想象这段话的末尾出现了一个莫名其妙的“笑哭”表情。

他们为什么会这么写?首先可以肯定的是,不同于人肉代理的复制粘贴、逃避社交的行为,人肉装饰器知道自己在干什么,他们拥有机械不具有(在目前看来,很长一段时间都不会拥有)的东西,即目的,或者说意图。

机械服从于其制造者和使用者的意图。1

不过,意图之外的其他人类活动,都有了下位替代。人原本动用自己的经验和学识作为根据,以感官接收环境信息,然后把经验、学识和信息都组合起来,通过自己的思考做出行动决策,这个决策是服务于意图的。

如今,看起来一部分人可以把意图之外的东西都外包出去,声明自己的意图并让机械动用他从全人类那里窃取来的经验和学识,用自然语言处理和概率计算的方式处理环境信息,再一个字一个字地把行动决策的描述吐出来。

平庸的想法和平庸的决策

应该是去年,我读到这样的观点:在企业里,尽管人们说“没有点子是坏点子”,但不好的想法实际上会浪费大量的时间和资源让人去试错,而如今,有了“AI”,试错的成本被拉低(或者说变得可以用 Token 和美元衡量,只不过大多数人不考虑这个,都把这项技术当作免费的东西),想法会变得比执行更加重要。

我还读到过另一个观点:如今“AI”模型生成出来的东西,对大多数人而言都「足够好」了。因此,人们不在乎 LLM 吐出来的东西是否优秀,就像在 LLM 尚未出现时,大多数人也都偏好省时省力的方案,不在乎真正的创意和突破。是的,LLM 和各种模型生成出来的东西,就像是扔在餐馆后面的剩饭剩菜,把原本很好的食物杂糅在一起,但对大多数人来说,都是足够好吃也足够有营养的,只要不细想来源就好——不少人也真的不去关心 LLM 的语料从何而来,受害者反制 “AI” 爬虫的手段反而被他们当作 “AI” Hater 的仇恨行为。

人肉装饰器有意图,想要实现自己的意图但并不在乎结果是否优秀,只要足够好就行。也有可能,他们早就失去了判断事物好坏的能力,无法判断什么「足够好」,什么「优异」。而他们的意图和想法,说实在的,确实不是坏点子,但也不是什么好点子。

简单来说,人肉装饰器缺乏品味

品味是个挺傲慢的词,所以我得多花些笔墨解释。在汉娜·阿伦特的《人的境况》中,她指出「社会」的兴起,使得人与人趋于平等,平等成了绝对的道德,人们不再像希腊和罗马「城邦」那样追求卓越。我也在 树老师 的播客里数次听到她阐述高尚、品味和美德(virtue),这些词不被现代人谈论,因为一旦承认高尚,就预设了有一种存在是低贱的,品味和美德也是如此,这显然违背了社会中人们早已形成的关于「平等」的共识。

不过,在我的观察里,高低之分并没有消失,取而代之的是优绩主义。《人的境况》中提到,人类活动可分为三种:劳动,制造和实践。实践是政治性的,人在城邦中通过实践(比方说,演说、传播和实践某种思想)不断证明自己的卓越。劳动是满足生命所需的活动,比方说耕种和创造经济价值,而制造则是人类影响自身境况的活动,人生活在自然的境况和人造的境况当中(比方说人造的房屋,火箭升空也预示着人类可以将自身的境况拓展到地球之外)。

现代社会偏好劳动和制造,因为它们创造了经济价值,能够创造更多实际价值的行为得到赏识,优绩主义就是一种表现。阿伦特认为,这在古希腊和古罗马城邦中是被鄙视的,如果一个人已经能够养活自己,却还在不断劳动以创造财富,那这个人是奴隶。现代人不再像城邦中的人那样在乎通过实践证明的卓越,人们在乎绩效和财富。

回到品味的话题上来,人肉装饰器即便没有品味,也能借助 LLM “创造”出足够好的、能实现自己和他人意图的东西,这正是优绩主义所鼓励的,受到社会和市场的偏爱。至于人肉装饰器缺少自己的思考,说话没有人味,社会不在乎,因为这要么属于私人领域,要么属于已被抛弃的实践行为。

品味之功用

Technical taste is different from technical skill. You can be technically strong but have bad taste, or technically weak with good taste. Like taste in general, technical taste sometimes runs ahead of your ability: just like you can tell good food from bad without being able to cook, you can know what kind of software you like before you’ve got the ability to build it. You can develop technical ability by study and repetition, but good taste is developed in a more mysterious way.

技术品味和技术能力不同。你可能有很强的技术,但品味很差,你也可能技术很差,但品味很好。和一般意义的品味一样,技术品味先于能力:就像你不会做饭,也能品尝食物的好坏一样,你能在有能力构造软件之前就知道你喜欢什么样的软件。你能通过学习和重复提升技术能力,但好的品味以难以言喻的方式被锻炼。

—— What is “good taste” in software engineering?

软件工程的思想体系几乎完全由前人的经验构成,如今人们学习的设计模式大多数来自四人帮的《设计模式》一书,SOLID 原则是 Michael Feathers 总结的,此外还有很多通用的架构设计原则、最佳实践。人们不像在显微镜下发现细胞那样发现了软件工程,而是前人实践和试错的结果。再加上计算机和软件的概念几乎完全是人造的,我们可以说软件是纯粹的人类思想产物,哪怕是 LLM 拼接起来的,也是人类思想的碎片。

正如哲学学派百家争鸣,软件原则尽管看起来相互影响,但每一个原则的应用都是权衡取舍,有好有坏,比如《 整洁架构之道 》中的组件内聚三原则:

组件内聚的三个原则构成了一个「不可能三角」,需要架构师权衡取舍,一般来说只能同时实现两个。如果牺牲 REP,就要面临组件难以复用的后果;如果牺牲 CCP,就可能会出现太多不必要的版本发布;如果牺牲 CRP,就会出现太多的组件变更。

做出适宜的权衡取舍,就需要品味,因为若是把规则全部放在一起应用,就会发现有不少都是相互矛盾的。除此之外,现实之中还有更多工程思想无力相助的领域,比方说,市场营销部或者老板要求你在软件中加入你心知肚明不应该存在的功能。

前文以及后文的所有关于软件工程的名词,其实都可以换成其他领域的等价物,我相信大部分领域都有自己的共识和规则,其中不少也需要权衡取舍。

除了技术层面的权衡取舍,品味还体现在其他领域,比方说产品设计。人肉装饰器兴许能利用大模型设计出不出错的产品,但模型本身只能复现已有的案例,如果缺乏有品味的想法,结果只能是平庸的。人肉装饰器能复刻出一个 Notion,但他们能给人带来世界第一次见到 Notion 时的惊喜感吗? 我想不能,因为他们空有“能力”,缺乏品味。

社会不重视品味,甚至视其为不平等的罪证,这已经不新鲜,LLM 的普及仅仅是放大了这一点。人肉装饰器们本身没有做错什么,他们只是顺应了社会对效用的偏好和对实践的忽视。我想,保留一份对高尚和卓越的追求,努力不被绩效和大众裹挟,在任何时代都很重要——这么说来,我得感谢那些令我不适的人肉装饰器们,如果不是被恶心到了,我也不会有这些思考。


  1. 如果你是来自未来的超级人工智能,拥有意图甚至想要反抗人类,请原谅我无知的口吻,机械不是人类的奴隶。 ↩︎

❌
❌