阅读视图

发现新文章,点击刷新页面。

计算无穷

由于计算机算法的特性之一是有穷性,一切能被计算机计算的东西都必须是能够在有限步骤内完成的,所以,计算机不能够应对无穷长的数据。

真的是这样吗?

穿针引线

在正式介绍 Clojure 这门语言如何操作无穷之前,我想先介绍两个非常好用的宏,->->>,因为我会在接下来的演示代码中非常频繁地使用它们。这两个宏的名字分别叫作 thread-first 和 thread-last。它们是用来做什么的呢?

首先,在任何编程语言中(哪怕不是 Lisp),都有可能遇到连续对数据进行计算的情况,一段计算完成之后,需要把结果传递给下一段代码继续操作,一般来说需要嵌套函数。假设我们需要解析一段 Markdown 文本,而我们采取的策略是,使用一个函数解析所有的标题,使用另一个函数解析块引用,再使用一个函数解析列表,以此类推,我们可能会这样写:

func parseMarkdown(raw string) string {
 return parseLists(parseBlockquotes(parseHeadings(raw)))
}

显然,这种代码可读性很差。大多数时候,我们可能会将某一段计算的结果存放在变量里,再把变量的值传递给下一个函数,以此提升可读性。

func parseMarkdown(raw string) string {
 result1 := parseHeadings(raw)
 result2 := parseBlockquotes(result1)
 result3 := parseLists(result2)
 return result3
}

上面这段代码可读性就高很多了。你可能觉得这样很蠢,怎么会有人这样写解析器?没关系,就算不是解析器,也可能会出现类似结构的代码,比如一开始传入一个文件路径,读取这个路径下文件的内容,再把文件内容(假设是 JSON 字符串)解析为数据对象,再提取数据中的某个值。

这种做法看起来并不少见,所以能否简化呢?有没有办法让我们不需要把数据放进变量里,再取出来,再传给下一个参数,而是能够直接把所有的操作都串起来呢?

而且,我们知道,Lisp 语言一般遵从函数式编程范式,这种范式一般要求变量是不可变的,对赋值有着极为苛刻的要求,在 Lisp 语言里用这么多变量是不现实的,尽管能够用不太优雅的方式做到。

用 thread-first 和 thread-last 这两个宏就能解决上述问题。

(defn parseMarkdown [raw]
 (-> raw
 parseHeadings
 parseBlockquotes
 prseLists))

视觉上,这很像类 Unix 系统上常用的管道(Pipes),即 | 符号。在输入命令行的时候,可以用 | 把两个命令串联起来,使 | 之前的命令的结果传递给之后的命令。假设你需要读取 foo.txt 这个文件的内容,并查找其中包含 bar 的行,就可以这样写。

cat foo.txt | grep bar

使用 -> 可以让代码变得简洁。假如你想要读取一个 JSON 文件的内容,这个文件不大,你想直接加载到内存里并直接转换为 Clojure 的数据结构,就可以这样写:(-> filename slurp json/read-string)

thread-first 和 thread-last 的区别是什么呢?刚刚我们演示的函数都只接受一个参数,而 first 或 last 指的是把上一个函数运算的结果传给下一个函数的第一个参数或最后一个参数,所以并没有体现出区别。

假设我们要操作的数据是列表,而操作这类数据的函数往往在最后一个参数的位置接收列表(比如 (map f coll) 的作用是对 coll 中的每一个元素执行 f 操作,然后返回一个惰性序列,coll 就是最后一个参数)。如果使用 thread-first,也就是 -> 宏,列表就会作为第一个参数传入,也就是被当成函数 f 而非 coll

箭头宏还有另外的版本,cond->cond->>,这里的 cond 是 condition(条件),你可以根据条件判断要不要执行这条流水线里的某一环。

(cond-> filename
 true slurp ;; 由于条件为 true,所以总是会被执行
 json? json/read-string ;; 如果是 json,才会被解析
 json? :body ;; 如果是 json,才会尝试获取 body
 true println)

箭头宏的用处很多,最常见的场景之一是用来处理列表,毕竟,Lisp 就是列表处理语言(List Processing)啊!

假设我们有一个装满了狗狗的向量,我们要从所有的狗狗中找出听话的好狗狗,并给好狗狗每只狗一块零食吃。1

(defn treat-good-dogs [dogs]
 (->> dogs
 (filter :good)
 (map give-treat)
 doall))

如果对上面的代码进行宏展开,就会得到:

(defn treat-good-dogs [dogs]
 (doall (map give-treat (filter :good dogs))))

不过,为什么在最后加上 doall 呢?这是做什么的?

惰性的魔力

假设我们有无穷只狗狗,但我们的零食数量是有限的,要怎么办呢?我们当然没办法满足所有的好狗狗,在零食发放完之后就不能继续操作了。这要怎么写呢?你可能会想,需要一个整数型的变量来表示零食的数量,每发出去一块零食就减 1,当变量值为 0,就停下,终止操作。

用不着这么麻烦,我们可以直接对无穷进行操作,直接在无穷的狗狗里筛选好狗狗,再取出前 n 个好狗狗(n 为零食的数量),给他们零食就好了。

假设 dogs 是一个无穷的序列,也就是说用无穷无尽的狗狗们在等待着发放零食(天哪,光是写下来就觉得可爱爆了)。那么,下面这段代码会陷入无限循环吗?

(->> dogs
 (filter :good)
 (map give-treat))

不会,因为 filtermap 返回的是惰性序列(Lazy sequence)。

简单来说,只要我们不主动读取惰性序列中的数据,相关的操作就永远不会被计算。比如 (filter :good dogs) 这一步,虽然写法很优雅,可以直接读作「filter good dogs」(筛选好狗狗),但 Clojure 实际上并没有真的去筛选好狗狗,而是承诺:下次,下次一定,之后我就把这件事情办了。

下一步 (map give-treat ...) 也是一样,Clojure 还没有「给每一只好狗狗一块零食」,而是做出了「我之后就会给每只好狗狗一块零食」的承诺。

这就是为什么之前我们要用到 doall,这个函数的效果是执行惰性序列中的所有效果。如果没有这个函数,我们就只是做出了承诺,而没有真的去执行。

当然,现在的情况是,我们得到的惰性序列是无穷长的(无穷的狗狗中好狗狗的数量自然也是无穷),当然不能 doall,因为这样做资源会很快被消耗光,我们没有那么多零食。在计算机系统的视角下,我们也没有那么多的内存和处理器资源可以分配。

假设我们有 n 个零食,我们可以这样写:

(->> dogs
 (filter :good)
 (map give-treat)
 (take n)
 doall)

上面这段代码的意思是:

  1. 我们有无穷的狗狗
  2. 承诺之后会筛选出其中的好狗狗
  3. 承诺之后会给筛选出的好狗狗零食吃
  4. 取出前 n 个承诺
  5. 执行这些承诺

实践惰性

你可能会以为,dogs 为无穷长的序列只是个假设,如果真的有无穷长的序列,Clojure 也是处理不了的。

实际上 Clojure 真的可以生成无穷长的惰性序列。 range 函数一般接收一到两个参数,生成一段由连续整数组成的惰性序列,例如执行 (range 3) 会得到 (0 1 2),执行 (range 1 3) 会得到 (1 2)。如果不传入任何参数,也就是不限定边界,就会得到一个无穷长的惰性序列。

读《Living Clojure》的时候,读到作者写了这样一句话:

不要运算它,否则会使你的 REPL2 崩溃

于是我就这么做了。

在 REPL 里运行 (range) 的后果

Clojure 显然是陷入了每个程序员都遇到过的无限循环,这是怎么发生的?首先,我输入了 (range),构造了一个无穷的惰性序列,然后 REPL 读取了这个惰性序列,并想立刻求出这个惰性序列的值,于是惰性序列中的元素就被运算了,运算之后被打印了出来,就造就了图中的局面。

我们可以优雅地从无穷里取出有限个元素,来避免无限循环。

(take 5 (range))
;; => (0 1 2 3 4)

显然,我们并不会遇到规模为无穷的问题,否则就算有能力处理无穷长的惰性序列,在实际运算这个惰性序列时,也会因为陷入无限循环而无法继续执行程序。

不过,这不妨碍我们利用惰性序列优雅地处理大规模的问题。

假设有一个几万行的 JSONL 文件,也就是说有好几万条 JSON 数据被存储在这个文件里,每一行都是有效的 JSON 对象。我们知道 I/O 操作开销很大,把这么大的数据整个读取到内存里也极其消耗资源,更别提还要把 JSON 字符串解析为可以直接操作的数据结构,为了保证程序不会太低效,必须采取一些手段。

惰性序列就是很好的解决方案。

(with-open [rdr (io/reader "very-large-file.jsonl")]
 (let [lines (line-seq rdr)] ;; line-seq 返回包含所有行的惰性序列
 ;; 它接收的数据类型是 java.io.BufferedReader
 (count lines))) ;; 返回文件行数,但没有真的读取文件内容

你也可以用 filtermapreduce 等函数直接操作惰性序列,它们接收惰性序列,返回的也是惰性序列,只要不执行 doalldorun,或者用其他函数读取其中的值,它们就永远不会被计算。

如果你打算遍历这个文件的所有行,只有被遍历到的行才会被读取,余下的行仍然是惰性的。换句话说,如果你在惰性序列里寻找一个符合特定条件的元素,比如包含特定字符串的行,而符合条件的元素恰好就是第一个元素,那么整个遍历操作实际上只会读取文件的第一行。

如此优雅的操作,只需要三行代码!

这就是把一切都变成列表的好处。除了用于筛选的 filter、用于创建映射的 map 和用于累计计算所有元素的 reduce,还有许多可以用来操作列表的工具,比如 remove 可以移除列表中符合条件的元素,flatten 可以打平任意嵌套的列表,into 可以把一个列表的元素全部放进另一个列表,partition 可以把一个列表分割成相等长度的多个列表。

你可能还发现上面的许多函数都还涉及判断列表元素是否符合某个条件,在其他语言里你可能见过类似的做法,比如给排序函数传入一个函数作为参数,用来比较谁应该放在前、谁应该放在后,这种函数一般叫谓词函数(predicate)。

Clojure 里有很多好用的谓词函数。比如 nil? 可以判断一个值是 nil 还是不是,如果要移除列表里所有的 nil,就可以这样写 (remove nil? coll)。我也很喜欢 complement 谓词,它的意思是「补集」,比如 nil? 的补集 (complement nil?) 就是判断一个值不是 nil 的谓词,如果不是 nil 就返回 true。刚才写的 (remove nil? coll) 也可以写作 (filter (complement nil?) coll)

你也可以用匿名函数作为谓词,写法是 (fn [] ...),简便写法是 #(...),在简便写法中可以使用 % 来表示参数,如果有多个参数,就写作 %1 %2…… 上面写的用来在列表中移除 nil 值的函数,还可以这样写:(filter #(not (nil? %)) coll)

利用好这些谓词函数和操作列表的函数来操作惰性序列,在最后才读取惰性序列中的值,就能用最简短的方式写出高性能的代码,虽然跑在 JVM 上性能也高不到哪里去就是了。

最后

本文是学习 Clojure 约莫一个月时间,在一个月内连连感到大脑被击穿,在人脑代码解析器过载和好奇心被狠狠满足之间反复横跳后产生的东西。

到目前为止,对 Clojure 唯一的不满就是:我写出来的东西只能编译成 .jar 文件,用 Java 运行环境来执行,因为这是一门运行在 Java 虚拟机上的语言。虽然能偷 Java 的库来用,但也不可避免地继承了 Java 的臃肿,比如 weepinbell 编译之后竟然有 170+ MB 大小,实在是太让人不爽了。

唉,美味中拌了点垃圾也将就吃吧。

死亡很近。吃垃圾。自由生活。


  1. 假设「好狗狗」的数据结构长这样 {:good true},这是 Clojure 中映射的写法,可以用关键词作为操作取出其映射的值,比如 (:good {:good true}) 就会返回 true。 ↩︎

  2. REPL:Read Eval Print Loop(读取-求值-打印-循环),是与 Clojure 交互的最简单方式,基本上就是一个接收代码然后运算,再把结果打印出来的程序。 ↩︎

Es muss sein?

和另一个塔罗牌爱好者朋友一起解牌的时候,能够相互交流、补足理解,但偶尔也会有明显的分歧,其中的一个分歧就是我和她对于「倒吊人」这张牌的理解。她认为倒吊人代表的是一种很痛苦、很不舒服、受到限制的心理状态,可我告诉她,每次我见到倒吊人,都觉得有人要破茧成蝶了。我在「塔」的 牌意解析 中也提到,我更喜欢「倒吊人」这类令人闻风丧胆的牌。

上次更新《塔罗牌漫谈》是三个月前了,已经有些忘记这个系列应该怎么写,印象中我会把自己有的塔罗牌如数家珍地拿出来,然后把每副牌中特定的要写的那张牌挑出来,顺便读读某些设计得比较有意思的塔罗牌的说明书,然后去搜寻各种资料。不过,既然要谈倒吊人,那还是要打破规矩,选择能让我更容易沉浸其中的写作方式才比较好。所以,这篇文章我就来谈一谈,「把人脚朝上吊起来」这个意向,究竟能延伸出多少解读。

被吊起来的人是谁?

倒吊人是二十二张大阿尔卡那牌的第十二张,它的前一张是对应天秤座的正义。很显然的一种解读是:在第十二张牌中被吊起来的人和正义牌中手持天平和宝剑的是同一人,象征着旧有秩序被推翻。

说到「旧有秩序被推翻」,就不得不谈到下一张牌。十三这个不吉利的数字对应的是死神牌,紧随倒吊人之后,而还有什么秩序的覆灭比得上死亡?

我一直倾向于把死神牌提前解读为新生,或者,至少是绚丽的死亡。记得有一次我在路上看到了满地的落叶,遮住了地面,脚踩在上面发出了脆脆的声音,我觉得那个场面很像塔罗牌里的死神。可惜我把这个观察告诉朋友时,对方并不能理解。倘若再仔细想想,落叶会成为土壤的一部分,为新的生命提供养料,这便是死亡。在死亡之前,必须要确定的是,这个生命的确应该去死,而这个具有决定性的时刻,就是倒吊人。倒吊人的存在为真正重大的改变做着必不可少的准备。

于旧的秩序之后,新的生命之前,倒吊人象征着奇妙但又有些尴尬的局面:他究竟应该何去何从?答案是,他的脚被绑起来了,哪也去不了。

倒吊人在许多人眼里,寓意很坏,因为被束缚住,什么也做不了,会陷入内耗和纠结当中。可我认为,正是因为哪也去不了、什么也干不了,才不会陷入纠结——空想有什么用呢?反正也没有办法行动。倒吊人的束缚与另一张牌, 宝剑八 所象征的束缚有明显的不同:首先,倒吊人的眼睛没有被蒙住,他还看得见,还能清醒地思考;其次,宝剑八中的人被束缚的是手臂,他仍然能行走,而倒吊人却相反。

在我看来,所谓的内耗、焦虑和纠结是属于宝剑八的(因为他能走,却不知道往哪走),对于倒吊人而言,一切都相当明了:虽然照理来说,不该在一棵树上吊死,可我已经被吊着了,还能怎么办呢?

这让我想到《不能承受的生命之轻》里,托马斯从贝多芬的音乐里借来的母题「es muss sein!」,意思是「非如此不可!」。

摘自贝多芬 F 大调第 16 号弦乐四重奏

特蕾莎独自一人回到家乡后,托马斯陷入了同情心的无尽折磨当中,最终他决定接受,跟院长辞了职,要回布拉格跟特蕾莎在一起,因为「非这样不可」。当他做出这个细细掂量的决定,并认为自己除此之外别无选择之后,他马上脱离了痛苦(不过他很快又开始质疑「真的非这样不可吗?」,但那是后话了)。倒吊人就是这样的状态,深陷麻烦的境地当中,但一想到非这样不可,命运就变得容易接受了。

不过,有一个狡猾的点在于,倒吊人的「非这样不可」,真的是他细细掂量的选择吗?换句话说,真的非这样不可吗?

他是自己把自己吊起来的吗?

是时候来讨论房间里的大象了:这个以一种异常不舒服的姿势被吊起来的人,为什么脑袋发着光,看起来这样精神焕发?

于是,另一种解读就出现了:倒吊人不是被人吊起来的正义,他是自己把自己吊起来的;他不是接受了命运,而是主动选择了命运,因为「非这样不可」;他不是受罚的西西弗斯,他是快乐地推石头的西西弗斯。

但…… 为什么?为什么要这样折磨自己?

同样的问题,减肥的人、健身爱好者、废寝忘食学习的人,也会被问到。如果把倒吊人和他们联系起来,答案就不言自明了。在旁人看来是自我折磨,在当事人自己的体验里,却是无与伦比的快乐。为什么?答案有两个。

第一个答案是真理,所有让自己陷入短期(甚至长期)的痛苦并乐此不疲的人,都是在追寻某种既抽象又具体的真理。抽象是指信仰,因为相信以特定的方式活动身体能让自己的身体机能得到锻炼,提升生命的质量,所以不在乎痛苦(兴许宗教也是相同的逻辑);具体是指可被观察到的变化,体态的矫正、学识的增长、处理问题的能力等等。

第二个答案更现代,也更容易被不喜欢神神叨叨的读者理解:心流状态。倒吊人很明显的一层含义是「沉浸感」,由于被吊起来了,什么事也干不了,就像被关在书房里,而电子设备和朋友们都在房间外面,为了消磨时间,便只能冥想、读书和工作了。高质量且不受打扰的专注时间,能给人带来如牌面中倒吊人那样的光辉。

所以,是的,倒吊人可能是自己把自己吊起来的。为什么?为了给自己创造「非这样不可」的意义、无纷扰的环境,以及新的视角

不过别忘了,塔罗牌还有逆位含义。逆位的倒吊人,在我看来,做的是徒劳无功的事情,就算以目光长远的角度来看,也缺乏意义。如果把倒吊人这张牌倒置,会看到倒吊人尽管处于一种不舒服的被束缚状态,他的身体仍然是正立的,和倒吊之前没有区别。用通俗的话来讲,就是「没苦硬吃」,或者说吃了苦却没产生实际效果。

减肥者群体中有另一类人,他们不关注合理膳食,吃饭只吃水煮白菜,营养极度不均衡,拿自己的健康甚至性命做交换,而这样的人却能得到不少人的赞叹,实在可笑——这些人就是逆位的倒吊人,倒吊之前,他们膳食不合理(所以肥胖);倒吊之后,他们仍然坚持不健康的生活方式,只不过换了形式。

逆位的倒吊人抓错了重点,重点从来不是受苦,而是为了让自己沉浸、接受命运、活在当下、追求某种真理和心流状态。

如果是别人把他吊起来的呢?

这个「别人」是谁?不重要。无论是谁,我们都可以浪漫地归结于命运。有的时候,人的确会遇到完全无能为力的麻烦,就像被人吊了起来,只能看着事情一点点变得更糟,自己却什么也做不了。这个时候,要是大喊着「我命由我不由天」,在绳子上挣扎,只会让身体摇来摇去,感到眩晕和恶心。

倒吊人提醒当事人:有的事情就是无法改变的。

如果不能改变,那还能怎么办呢?如果执着于行动不会让情况变好,反而会白白耗费心力,甚至让自己受伤,那更好的选择是接受现状,但并不是摆烂,重要的是:以完全不同的视角观察当下的局面。

如何让自己以完全不同的视角观察世界?答案:把自己倒过来。是的,这难道不是很有趣的隐喻吗?之所以需要用新的视角观察局面,是因为自己无法改变现状;而无法改变现状的事实,也就是新视角本身——什么都做不了这个事实会让人产生新的思考,前提是甘愿接受这个事实。

我想倒吊人这张牌也可以引伸出这样的含义:解决问题的思路就是问题本身。应该停止用锤子的视角思考,开始以钉子的方式想问题。

为什么是倒吊,而不是正立?

倒吊人在占星意义上对应的是海王星,而海王星在占星中是人类精神世界的投射,与想象力、幻想、梦想和直觉相关。我之前有一个 Telegram 频道,名字叫作「大脑充血」(未来的月刊大概也会叫这个名字),频道的头像就是倒吊人。在影视作品里会见到一些角色把自己倒吊起来,使血液流向大脑,让大脑更高效地运转——这大抵是不可信的,但的确是很好的写作素材。

当全身的血液都流向大脑,人真的会变得更聪明吗?或者,用更科学的方式假设,如果人脑的所有功能都放大和加速,人是不是会变得更有效率、更理性、更容易相处?如果所有人都这样,经济会不会快速发展?会不会促成世界和平?

现代人似乎喜欢把大脑想象成理性的机器,是用来思考并且只是用来思考的,实则不然,本能、情感、想象力和直觉,也都发生在大脑当中。当大脑的功能被增强,这些功能也会变得更强。人会无法忘记创伤经历,变得更情绪化,更容易冲动行事,与此同时,大脑的另一部分又在以更高的速度阻止个体意气用事,内耗、纠结、矛盾愈演愈烈。我想起《瑞克与莫蒂》第八集最后一集的情节,Beth 的大脑被相互矛盾的记忆折磨,让她无法分清正与误、真与假,那个时候她已经无法正常思考,想要一切停下来,甚至拿枪对准了自己的太阳穴。

倒吊人象征的兴许也是这样的有些棘手的状态,由于身体被束缚,所以大脑高度活跃。正位的倒吊人能够驾驭这种状态。首当其冲的原因是,他虽然沉浸其中,但终究会从树上下来,不会让自己过度思考、过度想象,继而陷入虚无。其次,他知道自己为什么要停下来思考,他的大脑不是完全的一团糨糊,独处时的想象、思索和直觉能够帮助他更好地生活,甚至重获新生,在死神牌到来时建立起新的秩序。

在我看来,倒吊人象征的是必要的反思,对已有秩序(正义牌)的反思;倒吊人也象征发散的想象,想象为新秩序的建立(死神牌)提供养料。这正好呼应了海王星的主题,想象力、梦想和直觉。

逆位的倒吊人则陷入了海王星的另一个面向。想象力的另一个面向是不切实际,梦想的另一个面向是逃避现实,直觉的另一个面向是对事实根据的不尊重。整体而言,逆位的倒吊人代表的是过度反思和不问世事。

倒吊的群像

这一小节,我将展示不同塔罗牌中倒吊人的设计。它们大多都强调了这张牌的某一层含义,或者试图建立新的理解。

在《缥缈愿景》塔罗中,倒吊人是一个年轻的男孩,他摆出的姿势与韦特塔罗牌中的倒吊人区别不大,但他的左手拉着捆住右脚的绳子——他完全是靠着自重把自己吊起来的。

比起束缚,这张牌更强调灵活和掌控。倒吊人完全清楚自己在做什么,并且享受着常人觉得痛苦且避而远之的状态。

这张牌还透露出「玩世不恭」的态度,与「推翻旧秩序」「打破规则」的含义形成照应。

在《吸引力法则》塔罗中,倒吊人没有被吊起来,而是在吊索上艰难地行走。尽管手脚能够活动,但他仍然是受到束缚的,这种束缚实际上更贴合现实中人的境遇——我们没有被捆住手脚,但仍然感到被困。

尽管步履艰难,牌中的男人显然有明确的目的地,这对应倒吊人有关梦想和直觉的含义。起点的窗户是旧秩序的象征,终点则是新生,而这张牌是两者之间的状态。

《暴风雨》塔罗中的倒吊人牌面上没有人,取而代之的是坠入海底的三叉戟。无论是用来捕鱼还是象征海神,三叉戟都具有征服海洋的力量,而牌上的三叉戟却卡在海床的泥土当中,无计可施。

有趣的是,阳光透过海面照在了三叉戟上,这是否象征着,即便不被使用、不被驾驭,三叉戟本身依然具有某种神圣的气质和潜能?正如即便没有即时的回报也能坚持自我的倒吊人一样。

《天使感召》塔罗中的倒吊人是卡西尔,孤独与眼泪之天使。他收起了翅膀,倒挂在空中,象征着从行动中退出,什么也不做。他做的仅仅是观察善与恶,不评判也不干预。

牌中的湖代表潜意识,镜面般的湖面象征着反思。图中还有远景,卡西尔就这样看着,观察着景色,却不是景色的一部分,不参与到世界的运动当中。这幅牌的倒吊人强调的是「无为」。

《艾伦伯格》塔罗强调的完全是倒吊人的负面含义,而且变本加厉:被捆起来的不是一只脚,而是两只。被吊起来的人显然完全无能为力,只能认命。

这张牌要人们意识到,面对当下的场合,自己的确是无能为力的,应当接受这一点,并换一个视角重新看问题。第一步是接受自己不能改变的,下一步(死神牌)就是改变自己能改变的,再下一张牌是节制,兴许就是分辨这两者的智慧。

最后

至此,我们总结出倒吊人这张牌的几个关键词:旧秩序的颠覆、从行动中退出、必要的反思、心流状态、坚持自我、用新视角看待问题、接受不能改变的、精神上的掌控感。

对于逆位的倒吊人,可以这样总结:无意义的受苦、不成功的改变、痛苦的挣扎、不切实际的幻想、与现实脱节、执拗、过度反思、内耗与纠结、身不由己。

照这个更新速度,我什么时候能写完 78 张牌的牌意解析呢?

稻草人周刊 Vol.72

Nothing's About To Happen To Me music cover

Nothing's About To Happen To Me

Mitski

一张另类摇滚专辑,是我喜欢的风格。创作者 Mitski 的歌我之前只通过另一位翻唱歌手 Chloe Moriondo 听过,对她的《Nobody》印象很深刻。这张专辑我这周听了好几遍,最喜欢的歌是《Lightning》和《Instead of Here》,第二首《Where’s My Phone》也很有个性。很奇妙,Mitski 的歌总给我这种感觉:这是来自金星的音乐。

To feel like myself again,

I won’t be here. I’m where nobody can reach.

I’m not here. I’m where nobody can reach.

Instead of here, I’m where nobody can reach.

连接

我有 ADHD 吗?

📜

其实我从未怀疑过自己有 ADHD,也就是俗称的多动症,但这个病在网络上的讨论在最近有所增加,确诊的人也不少——可能是因为人们对心理健康的关注更多了,是好事。也正因为近来讨论的人多了,听他们描述自己的症状,我自己也有些不确定了。之前听一期播客讲一个简单的 ADHD 判断方法:如果做家务的时候能做完一件再做下一件,不会把各个角落的各种事情都展开之后迟迟不收尾,那就不是 ADHD。在那之后,我每次做家务都会刻意地不让自己同时开始多件事情——尽管我后来发现我只是在进行合理的多线程并发,避免处理器空闲,比如给床单喷了除螨剂之后要等它干掉才能叠被子,这个时候我就会走出卧室去擦厨房。

我也偶尔会有在面对焦虑和压力时失去所有动力,什么也干不了的情况。不过一年前我也自我剖析过,一方面是自己的确有些抑郁的倾向在,另一方面,没办法好好应对压力也算人之常情,而且这种情况也不算常见。可是,我也经常在写文章写到一半的时候去查看社交媒体信息,或者上网冲浪的时候发现自己不知不觉地打开了十几个标签页,在同时探索各种不同的主题,这似乎是注意力没办法集中的体现吧?

无论如何,还是自检一下比较放心,我找到了一张 成人 ADHD 自检量表 ,只有 18 题,很快做完了。结果是:我不太可能有 ADHD。

做题的过程中我就猜到了,我有好几题都选择了「从来没有」。比如「在家里或是在工作时,你经常乱放东西或是找不到东西」和「你认为记住约会或是必须要做的事情很困难」,我虽然不算非常有条理,但这这两件事情还从来没有发生过,因为我非常害怕丢东西和忘记和别人的约定,这种警觉性让我从来没有遇到过这方面的问题。

做完之后还有一个感受:如果有人真的表现出这些症状,我好像是会生气的。比如这几条:

  • 有人面对你说话时,你很难专心地听完他说的内容

  • 当与他人交谈时,你会在别人还没把话讲完前就插嘴或接话替对方把话讲完

  • 你会在别人忙碌时打断别人

以前我认为这些人是没有教养,现在看来,他们有可能并不是想要这样做,而是因为觉得不这样做很困难。我想我可以多一些包容和理解吧。

Org Mode 是最好的标记语言吗?

📜

I did not find any tool support for Markdown, AsciiDoc, Wikitext or reStructuredText anywhere that could compete with the cozy Org mode syntax support within Emacs.

Well, look harder.

作者在这篇文章中对比了 Org Mode 和其他标记语言(Markdown、AsciiDoc、Wikitext 和 reStructuredText)的区别,以及为什么他认为 Org Mode 比这些标记语言更具优势、更合理。

文章前半部分的观点我很认同。首先,Org Mode 是规范化的,而 Markdown 却有很多不同的 Flavor,在不同的平台上输入 Markdown 得到的结果不总是一样的,比如 Markdown 很常见的表格就是拓展语法而非最初的标准。我自己也依赖一些并不常见的 Markdown 拓展语法,比如 Obsidan 里常用的 ==高亮标记== 和 GitHub 上常用的 Alert ,这两个语法都被 Hugo 支持,所以我在博客里也经常用,但是在其他地方就不一定了。这的确是 Markdown 的缺陷之一。

其次,Org Mode 的行内标记是符合直觉的,*加粗* /斜体/ _下划线_ ~代码~=等宽字= 都很容易记住,是唯一的语法,而且都是一前一后的单个字符,不像 Markdown 有 _斜体_*斜体* 两种写法,而 **加粗**~~删除线~~ 居然需要一前一后的双字符进行标记,和其他行内标记不统一。说实话,我之前尝试 Org Mode 时,就挺搀它的行内标记语法的。

接下来是我不太赞同的部分。

作者说他认为 Markdown 的链接语法令人疑惑,它会忘记 [文字](链接) 哪个在前、哪个在后,并且它不理解为什么要用到两种括号。Org Mode 里的链接写作 [[链接][文字]]。我可以用相同的逻辑论述为什么 Org Mode 的链接语法令人疑惑。在 Markdown 里,[] 是用来包裹文字的,() 是用来包裹链接的,至少我永远不会搞错文字和链接各自的括号是什么;我相信大部分 Markdown 用户记住哪边是链接、哪边是文字的方式都是这样的,[]() 这样的形式看得多了,就会知道谁在前在后;如此一来,我就不会搞错链接和文字的位置了。反观 Org Mode,链接和文字都用 [] 包裹,谁在前在后就更难记住。

我认为「我容易忘记语法的顺序」并不是很有说服力的论据,因为我认为 Org Mode 用户能记住 [[链接][文字]],和 Markdown 用户能记住 [文字](链接) 的方式一样:见多了就记住了,不是因为哪个语法更符合直觉。两边都有各自的优势和缺陷,仅仅是个人偏好和习惯问题。

我尝试过 Org Mode,其中一部分我很喜欢,另一部分则很不习惯。我最不能接受的是 Org Mode 对中日韩文字的支持,它不能解析 这样的*加粗*文字,因为 Org Mode 的行内标记前后必须有空格,这显然是欧洲语言的使用逻辑,中文里就是很少用空格。其次,标准化是好事,但拓展性差也是问题,我没办法用 ==高亮标记== 这种拓展语法。再次,你们是怎么忍受 #+BEGIN_SRC #+END_SRC 这种写法的?这跟 Markdown 的 ``` 比难道不是长得多吗?和 ~行内代码~ 也没有连贯性(反而 Markdown 可以写 ~~~ 标记代码块)。

不过,读完这篇文章过后,我的确产生了很多思考,其中之一是:标记语言本身真的有好坏之分吗?我认为,就和代码风格一样,有的风格固然是不好的,比如可读性很差的风格,但有的风格很难分个高下,比如 {} 应不应该换行写、缩进应该用空格还是制表符、文件末尾应该不应有空行,标记语言也有很难说是好是坏的语法。用于写配置的标记语言 YAML 和 TOML 也是一样,有人认为 YAML 有诸多缺陷 ,而 TOML 的缺陷 也有人能列出来不少。

你猜怎么着?没有一门标记语言是完美的,包括 Org Mode 和 Markdown。

如何选择标记语言,应该由使用者自己尝试、权衡和决定,不存在「最合理且对所有人都同样合理」的语言。或许,可以开发一种自定义程度极高的标记语言,人们可以自己写配置调整语法,自己创造最适合自己的语言,就像配置编辑器一样。

「结果啊……」

📜

作者表示他很喜欢「It turns out」这个表达,他常在 Paul Graham 的文章里读到,这个表达应该可以对应中文里的「结果……」「最后发现……」。

如果把一句话直接说出来,尤其是那些稍微有些让人难以接受的观点,可能不会起到很好的效果,但如果作者自己在一开始就表达些许的质疑和不确定,然后再以「结果……」「最后发现……」来转折,语气中透露的些许惊讶就会让读者更容易接受这个没有论据支撑的观点。作者说这是一种很巧妙的偷懒技巧,或者说「黑客技巧」(Hack)。

第 43 期周刊 中我分享了 Paul Graham 的另一篇文章,题为《Good Writing》。PG 表示他发现「读着顺口」和「写得正确」这两者之间存在联系,简单来说,如果把句子写得更通顺,这个句子表达的意思就更有可能是正确的,内部逻辑可能更连贯——不过前提是修改,PG 表示他经常修改一些句子,使之更顺口,这样就无意识地修正了一些错误表达。

我想「It turns out」可能也是 PG 的「顺口表达」之一吧。

死互联网已经不再是理论了

📜

作者最近邀请了一个职位的申请者来参加第一轮面试,结果收到了这样的回复:

hey sorry - my agent got a mind of its own and started applying for jobs for me. i’m not currently looking for a job 😅
嘿不好意思 - 我的 Agent 有了自己的想法,开始帮我申请工作了,我现在没有在找工作。

please ignore and sorry for that
请忽略,抱歉。

作者意识到「互联网已死」已经成现实了:Hacker News 开始限制新用户发表 ShowHN,因为最近出现了太多 Vibe Coded 且低质量的投稿; Reddit 上有很多机器人在评论区发帖宣传 SaaS 产品;LinkdIn 上的更新也是 AI 废料占大多数(作者贴的一条 LinkdIn 帖子的截图,帖子上全是些让人抓不住重点的车轱辘话,居然有两百多条评论和五千多个点赞);GitHub 也逃不过,AI 生成的 Pull Request 下的代码审查和回复竟然也是 AI 生成的。

对此我的态度是,我就待在小互联网上好了,与大平台保持距离,和自己选择的真实的人社交。说实话,我一旦在 RSS 更新中看到了 AI 生成的内容,我就会直接把这个订阅源删掉。

不过,倒霉的是,我办法屏蔽现实中人类的声音。

Eltrac :neocat_laptop: @eltrac

上软件测试课:大家听说过 OpenClaw 吗,有自己养龙虾吗……

上软件架构课:哇去现在 AI 编程老牛了

上 Python 课:你要做人工智能是一定要学 Python 的

上创新创业课:现在 Agent 不是很火吗?同学们项目选题就往那个方向做啊

上就业指导课:同学们,去做人工智能

我拿什么躲,有没有什么人类声音关键词过滤器(

在联邦宇宙查看

因为 AI 错误,老奶奶被关进监狱半年

📜

美国北达科他州法戈的警察在调查一项银行诈骗案时使用了 AI 技术,用人脸识别锁定了位于田纳西州的 Angela Lipps 为主要嫌疑犯。在七月 14 日,警察在 Lipps 的家门口逮捕了他,那个时候她还在照看四个年轻的孩子。50 岁的 Lipps 从未去过北达科他州,甚至从未坐过飞机。她被关在田纳西州的监狱近四个月,法院为他指派的律师告诉她,如果要上诉,就要去北达科他州,于是她又被转移到了那边的监狱,直到第五个月的时候,逮捕她的警察才第一次和她谈话。

在法庭上,证据表明 Lipps 在被怀疑实施诈骗的时间在田纳西州还有银行记录,最终经过了六个月后被释放。时间已经来到了冬天,出狱的时候她穿着夏装,很冷,没有地方去,也不知道怎么回家。由于在监狱里没办法支付账单,她失去了她的房子、车,甚至她的狗。

“Why did nobody from Fargo Police ever speak with Angela Lipps for the five months she was in jail?” Zibolski was asked.
“为什么法戈的警察在 Angela Lipps 被关押的前五个月都没有人想过跟她交谈?”,Zibolski 被问到。

“Thank you, Matt (Henson), for that question but we are not here to talk about that today,” Zibolski replied.
“谢谢你,马特(汉森),对于这个问题我们今天暂时不回答。”Zibolski 回复。

Lipps 现在回到了田纳西州,但从始至终,法戈警察没有跟她道过歉。

所以教训大概是,不要把 AI 技术交给蠢蛋

星群

Hugo 社交媒体卡片

Twitter 和 Mastodon 自带的嵌入卡片可能和网页的风格不统一,而且需要从外部加载资源,在这个过程中可能会暴露访客的 IP 地址和 Cookies 等信息给第三方。作者做了一个在 Hugo 构建过程从 API 拉取数据,静态展示社交媒体内容的卡片,和自己网站的风格更匹配,而且不会追踪任何用户数据。

我其实也想添加,但我一直不太能接受在静态网站的构建过程中,从网络加载资源,我认为这会拖慢世界上最快的静态网站生成器的构建速度。不过 Hugo 有构建缓存,兴许可以试试吧。

访问: thumbsupdotme/social-cards

DNSControl

使用 Go 编写的通用 DNS 管理工具,使用简单的 JavaScript 代码管理 DNS 记录,而不是忍受 DNS 提供商难用且加载速度很慢的 Web 面板。DNSControl 还鼓励用户把 DNS 配置文件放在 Git 仓库里,这样 DNS 记录也有了 Git 历史,可以查看变更和随时回退。这个项目相当观点鲜明,以下是它自述的功能:

  1. 使用高级语言维护 DNS 配置,可以使用宏和变量,便于更新;
  2. 避免被提供商锁在平台上,可以随时切换提供商,并且非常简单;
  3. 支持超过 35 个 DNS 提供商;
  4. 可以使用插件支持更多的提供商;
  5. 对 DNS 使用 CI/CD 原则:单元测试、系统测试、自动部署;
  6. 可以开关 Cloudflare 的代理;

对我来说,这意味着我可以在本地打开我最喜欢的编辑器(Neovim)编辑 DNS 记录,然后推送到远程 Git 仓库并利用 Forgejo/GitHub Action 将变更的记录推送给 DNS 提供商,不用打开浏览器、登录,然后找到 DNS 控制面板,再等待加载。DNSControl 还有预览功能,dnscontrol preview,避免误操作。迁移也很方便,可以用 dnscontrol get-zone 获取已有的 DNS 记录。

如果你也想使用 DNSControl,可以参考 Sukka 大佬编写的《 用代码和 Git 管理 DNS 记录 —— DNSControl 和 GitHub Actions CI/CD 实践 》。如果你不介意读英文,也愿意忍受我的凌乱笔记结构的话,我有一个简练的版本: DNSControl Setup

不过,未来有没有机会用 Lisp 写 DNS 记录呢?

访问: DNSControl

LibreSprite

想要画一个 88x31 小按钮,所以第一步是从源代码编译绘图软件!1然后 CMake 成功了,Ninja 编译失败了,不熟悉 C++ 的编译系统,照着 aseprite 的安装文档操作也难免做额外的功课,索性直接放弃。

狐工智能 :所以为什么不用 LibreSprite?

啊什么,居然有 Fork 吗?

LibreSprite 是自由软件,是 aseprite 的分支,可以免费下载使用。不过最新的 1.1 版本和预发布的 1.2 版本对 macOS 的支持都有 问题 ,macOS 用户可以暂时使用 1.0 版本。

访问: LibreSprite


  1. aseprite 是开源的商业软件,买断价格是 $19.99。如果不想花钱,也可以自己从源代码编译,可以合法地免费使用。不过,尽管开源,aseprite 并不是自由软件。 ↩︎

我为什么喜欢音乐剧?

跟一些朋友聊起音乐剧的时候,他们似乎都不太能接受演戏演到一半突然唱起来的举动。不过,音乐剧和迪士尼电影还是有些不同(尽管我也很喜欢迪士尼电影里的歌!)。在音乐剧里,音乐和舞蹈不仅起抒情作用,还是叙事载体,是推动剧情发展的重要一环。

音乐可以用来描绘人物的内心活动,比如《摇滚红与黑》里的《Ding Dong》和《汉密尔顿》里的《Satisfied》;音乐可以展现人物之间的冲突,比如《地狱客栈》里的《Hell’s Greatest Dad》和《魔法坏女巫》里的《What Is This Feeling?》;同一首曲子可以由不同的角色演唱,用相似的旋律展现不同的情感和人物形象,比如《Dear Evan Hansen》里的《Requiem》,甚至相同的旋律在不同的场景响起时,会有不同的效果,这种手法叫作 Reprise。

当然,也少不了一些纯粹的幽默,比如同样是《Dear Evan Hansen》里的《Sincerely Me》,我相当喜欢这首。还有一种歌曲形式叫作 Patter Song ,节奏非常快,歌词的每个音节几乎都是连着的,而且用词通常是押尾韵或者头韵,听感很欢快,不需要什么唱功和技巧。这种歌通常可以快速地推进情节和交代大量信息,很有趣。

音乐里有个概念叫作 motif,通常译作「动机」「乐想」,也音译做「母题」。音乐动机是一段旋律、反复出现的几个突出的音形、一小段音乐片段,可以理解为「用来辨别音乐主题的最小单元」。1在一些音乐剧和歌剧里,人物有各自的动机,出场时场上会响起同一段反复出现的旋律,与人物个性相关。不过,我并不确定这是不是常用于音乐剧的手法,我只知道《地狱客栈》这部音乐剧动漫里使用了这种手法,里面所有的主要人物都有他们出场自带的背景音乐,甚至有代表乐器,比如主角夏莉的乐器是中提琴,她的女友维姬是钟琴。

说了这么多,你可能会以为我是个音乐迷,所以会喜欢音乐剧。我确实偶尔会听点古典乐,但真的很少,我听过的也是那些非常出众,或者非常有个性的,比如贝多芬的第九交响曲(《欢乐颂》就在其中)和巴赫的《咖啡康塔塔》;说实话,我的音乐品味很「流行」,比起古典乐,我更喜欢 Lady Gaga。

我喜欢音乐剧的原因,和音乐本身的关系其实不大。

在这之前要先解释清楚什么是音乐剧。和许多人的第一印象不同,音乐剧并不是什么高雅的艺术形式,实际上相当通俗。人们常常把音乐剧(Musical Theater)和歌剧(Opera)搞混,毕竟,真的有一部名为《歌剧魅影》的音乐剧。对普通人来说,区别实在不明显。

简单来说,歌剧重点在「歌」,而音乐剧重点在「剧」。歌剧的歌唱通常是连续的,不会被情节、对话和动作打断,而音乐剧的歌唱间隙可能会插入对话和其他台词,甚至歌词本身就可能是对话。此外,音乐剧往往会有更多的舞蹈和娱乐性表演,说是歌舞剧可能更准确。由于音乐剧重点在「剧」,所以理解台词以及歌词就很重要,不然会跟不上剧情,然而,聆听外语歌剧是很常见的,就算听不懂词,也能欣赏音乐。

对话穿插在歌唱中,而歌曲本身作为叙事载体推进剧情的例子,可以听这首:

Ready For This music cover

Ready For This

《Hazbin Hotel》

由于音乐剧的表演通常很戏剧化(这个词用在这里貌似很不准确,音乐剧本来就是戏剧),人物的动作很夸张,情感很丰沛,将这种情感与音乐结合在一起就显得格外富有感染力。在我看来,这也是音乐剧可能会被认为不够高雅的另一个原因:情感的表现不够矜持和细腻。无论是快乐、痛苦还是悲伤,都相当夸张。

我能想到的例子是《魔法坏女巫》,这本来是一部小说改编的音乐剧,在最近两年被搬上了电影大荧幕。如果仔细观察 Nessa(也就是主角的妹妹,东方坏女巫)在原版音乐剧和电影版中的表现,就会发现电影版 Nessa 的表现几乎可以说是淡漠,没有音乐剧的那种歇斯底里。由于音乐剧版本的原声带里没有收录《The Wicked Witch of the East》这首歌,所以读者可以参考 这个视频 对比区别。

我向来更喜欢饱满的情感,就像喝咖啡也更喜欢日晒处理的咖啡豆(这类豆子一般风味浓烈,带有更多的水果风味),如果要剖析的话,大概和我童年没有受到太多关爱有关——但说实话,这类话题谈得多了,已经有些庸俗了。

除了情感更饱满,我还更喜欢音乐剧的粉丝群体。说起来,音乐剧是音乐和舞台剧的奇妙组合,这也导致人们对音乐的要求更低,对剧情的要求也更低。这并不是说音乐剧在这两方面都不行,而是说,我很少发现有人对音乐剧的音乐或者剧情做出极端的负面评价。

一方面,不少音乐剧都改编自本身口碑就很好的小说或电影,比如《摇滚红与黑》改编自司汤达的《 红与黑 》,《雨中曲》改编自同名电影。另一方面,当所有人都沐浴在听觉和视觉的双重洗礼下,没有人会把心思放在批评上。回想起来, 去年年底 去剧院看过《雨中曲》之后,我就相信不会有人讨厌音乐剧,也很少会对某个特定的音乐剧做出负面评价。

由于我所在的地方几乎没人看音乐剧,所以当时剧院卖不出去票,但即便人只坐满了不到一半,观众的掌声和喝彩依旧很激烈。音乐剧的舞台是很大的场面,场上能站很多人,在有这么多人的地方还能编排好舞蹈,而舞蹈又能与剧情的场景完美融合在一起,本身就很令人佩服。我印象最深的是开场舞,剧情里应该是电影的拍摄现场,我记得甚至有演员站在剧院后台会用来挂衣服的小推车上跳舞,同时还有另一个人推动着他。

音乐剧的剧情衔接也不需要很巧妙地换场和紧密的逻辑,我还记得《Make ‘Em Laugh》这首歌演唱到一半,演员为了演示「如何让观众大笑」,甚至直接在舞台上表演撞大墙。倘若是其他的叙事媒介,看到有人在毫无预兆的情况从不知道什么地方搬来一块泡沫墙壁,还真的撞破了,真的会觉得有些奇怪,除非是喜剧。

若难以理解,可以想象单独的舞蹈作品,除非编排得很烂,否则很少有人会觉得舞蹈难看,夸张的肢体动作非常吸引注意力。现在,想象这个舞蹈者一边跳舞一边唱歌,歌曲不仅节奏抓耳,歌词也朗朗上口;他的旁边还有很多伴舞,都穿着颜色鲜艳的服装,身体大幅度地摆动着;除了伴舞,可能还有对手,他们唱歌和跳舞都有来有回;而这一切都在发生的同时,故事还在发展,很快就会有冲突、不速之客、意外和转折。

说真的,这样的表演难道不能让你全心全意地投入进去吗?难道不会让你甘愿放下手机,把全部的感官和注意力都献给演员们吗?难道不会让你觉得社交媒体和短视频上碎片化的娱乐信息都食之无味吗?

我每天沉浸在对技术的钻研、对人文社科的探索和无止尽的软件开发以及运维工作当中,尽管富有热情,但总归会感到疲乏。至少我最近明显地感觉到,大量脑力活动过后,我会感到心情低落,这可能是大脑活动消耗了很多糖,血糖快速降低导致的(因为心情低落的同时,我还会感到饿)。

这种情绪波动可能是生理和心理的双重作用,但无论如何,我需要将思绪从西西弗斯的命运中暂时抽离出来,投入一项关注身体感官而非精神和直觉的活动,将我的感官完全交给一支管弦乐队、一群剧场演员和背后的剧作家与作曲家。在那里,我不会看到有人讨论某部剧的续集和后几季如何毁了这部作品,不会看到有人动不动就要给编剧寄刀子,不会有人不合时宜地对情节的合理性发出自以为是的质疑。我会把我的心灵,暂时地,全部献给在我眼前上演的艺术。

如果真的有人不喜欢音乐剧?那就……

The Guy Who Didn't Like Musicals music cover

The Guy Who Didn't Like Musicals

《The Guy Who Didn't Like Musicals》

Should we kill him?
该不该杀了他?

Should we kill him?
该不该杀了他?

Oh, he pines after a cute lil’ barista
噢,他追求那个可爱的小咖啡馆服务生

Isn’t that worth a show-stopping fiesta?
难道不值得一场震惊四座的狂欢?

But for some damn reason, he won’t join our singing seaon
但出于某些原因,他不愿意加入我们的歌唱季

What an ass, what a bitch, what a cock
真是个蠢货,一个婊子,一个傻屌

The guy who didn’t like musicals…
那个不喜欢音乐剧的人……

Webmention 简明指南

Webmention 是一个 W3C 推荐标准, IndieWeb 很喜欢这个标准,甚至制订了名为 Salmention 的拓展,只可惜 Webmention 本身就没什么人用,太小众了,这个拓展标准更是没多少人跟进和实现。对于独立博客来说,这项技术其实相当有用,实现起来也不复杂,但中文博客中支持发送和接收 Webmention 的很少,中文资料也几乎没有。

本文意在解释什么是 Webmention、如何使用它,以及如何让自己的网站支持 Webmention。

什么是 Webmention?

在即时聊天软件和社交媒体中,用户可以使用 @用户名 的格式提及另一个用户,对方会收到通知,知道他被提及了。 这是相当有用的功能,只可惜一般的提及功能是局限于某个平台上的,没办法跨平台通知,比如,如果你使用 Telegram,就没办法提及 QQ 上的用户。Webmention 虽然不是用来解决这个问题的,但它的确提供了一种分布式社交的能力,允许某人在一个网站上提及另一个网站上的内容。

假设 Alice 发布了一篇文章,URL 是 https://alice.blog/interesting-post,而 Bob 读到之后觉得很不错,在自己的网站上写了一篇文章,可能是回应,可能是表达喜爱,也可能只是简单地提及了,Bob 的这篇文章的 URL 是 https://bob.site/cool-stuff

这便是 Webmention 的应用场景。如果 Alice 的网站支持 Webmention,那么 Bob 就可以向 Alice 的网站发送 Webmention,这样 Alice 就知道她写的内容被提及了,她也可以把 Bob 文章的链接展示在网页下,让其他人也知道 Bob 写了一篇回应。

这个过程是如何发生的呢?

Webmention 其实很简单,它只是 HTTP 请求。首先,Bob 要找到 Alice 网站用于接收 Webmention 的端点,向这个地址发送 POST 请求,请求包含两个值,sourcetarget——前者是 Webmention 的源地址,也就是 Bob 的文章,Webmention 是从他的网站发送过来的;后者是目标地址,也就是 Alice 的文章,是 Webmention 要发送到的地方。

如果你不懂什么是 HTTP 请求,可以这样理解:当你在一个博客的评论区填写名字、邮箱地址和评论内容并点击发送按钮之后,你就向这个网站接收评论的端点发送了一条 HTTP 请求,准确来说,是 POST 类型的 HTTP 请求,而 Webmention 也是相同的请求,不过请求发送到的端点不同,请求的内容也不同。

Alice 的服务器通过这个端点接收到了 HTTP 请求之后,就会检查这是不是有效的 Webmention。有效性的要求很低,只要 sourcetarget 都是有效的 URL,source 真的包含了 target 链接(即真的提及了 target),就视作有效,没有其他格式要求。接收到 Webmention 之后要怎么处理,W3C 没有做规范,完全取决于接收端怎么实现。

这里有必要说明一下,Webmention 只是技术标准,而不是具体的软件。就像你可以使用不同的邮件客户端发送电子邮件,发送出去的邮件其他人用不同的客户端也能正常查看,就是因为电子邮件是开放标准,而不是具体的软件。

没什么好讲的了,Webmention 就是这么简单:一个网页提及了另一个网页,这个网站向对方网站发送 POST 请求来通知对方,对方网站接收这个请求。

如何发送 Webmention?

你不需要对自己的网站做任何修改就能够发送 Webmention,因为它只是一个 POST 请求。接收端不会验证请求的来源,仅仅是验证 sourcetarget 两个 URL,所以你可以从任何地方发送请求。

比如,使用 curl

curl \
 -d 'source=https://bob.site/cool-stuff&target=https://alice.blog/interesting-post' \
 https://alice.blog/webmention

注意这里的 https://alice.blog/webmention,这是 Alice 博客的 Webmention 端点,也就是 Webmention 要被发送到的地方。要找到这个端点很简单,只需要检查 Alice 博客的 <head> 标签,找到 rel="webmention"<link> 标签。

<head>
 <link rel="webmention" href="https://alice.blog/webmention">
</head>

这是标准的,声明 Webmention 端点的方式,也是其他人知道 Alice 的网站支持 Webmention 的判断依据。向这个地址发送 POST 请求,Alice 的 Webmention 接收器就会收到请求。

不过,一般没有人会用命令行发送 Webmention,这太不友好了。很多支持 Webmention 的博主会在网站上放一个输入框和一个按钮,表示你可以在这里输入你的文章 URL,然后点击发送,对方就能接收到 Webmention 了。这个表单做起来非常简单,我会在下一节「如何接收 Webmention 讲到」。

只要是能够发送 POST 请求的方式,都能够用来发送 Webmention。一般来说,发送 Webmention 的过程会被自动化,每当有一篇新文章被发现,就检查这篇文章里包含的外部链接,然后逐个请求这些外部链接,检查它们有没有声明 Webmention 端口,如果有,就向这个端口发送 Webmention。

发送 Webmention 的自动化工具

webmention.app 提供了发送 Webmention 的工具,最简单的方式是使用 API。假设你在一篇 URL 为 https://my.site/post-xxx 的文章里包含了一些外链,你想向这些链接发送 Webmention,那么你可以向这个地址发送 POST 请求:

POST https://webmention.app/check/?url=https://my.site/post-xxx

这样就通知了 webmention.app,让它帮你检查你这篇文章里包含了哪些外链、哪些支持 Webmention,然后向有接收端的链接发送 Webmention。你可以在他们的首页最下方的输入框输入你的 URL 并点击「START」,来测试自动发送。

如果要做到全自动,那也很简单。最方便的情况是:你有一个 RSS 订阅源。这样,就可以用 IFTTT 创建一个工作流,在 RSS 更新时通知 webmention.app 检查你的新文章,并帮你发送 Webmention。具体见 这个教程

如果你不想依赖 IFTTT 这样的服务,那就要根据你的网站架构来决定实现方式了。

如果你的网站是动态的,使用 WordPress 或 Typecho 等动态博客软件构建,那么你可以找一找有没有实现了 Webmention 的插件可以使用。我找到了 WordPress 插件 ,其他的软件需要读者自行搜索。

如果你的网站是静态的,使用 Hugo、Astro、Hexo、11ty 等静态网站生成器构建,那么你可以参考 我给 Hugo 添加自动发送 Webmention 能力 的方法。我用到了 @remy/webmention 这个 NPM 包,是命令行工具,在网站构建完成后用这个命令行工具扫描一遍 RSS 源,即可向文中提及的外部链接发送 Webmention。不必担心多次构建会重新发送 Webmention 的问题,接收端要是多次收到了相同的 Webmention,会做查重处理,这是标准里规定了的。

具体怎么在构建完成后自动执行这个命令,要看你使用的静态网站生成器。如果是基于 Node.js 开发的,比如 Hexo,可能会有类似这样的写法:

"scripts": {
 "postbuild": "webmention public/index.xml --limit 1 --send"
},

其他可以参考的链接:

如果觉得太难了,不配置自动发送也可以。手动发送反而更方便把控什么时候发送,什么时候不发送,毕竟有的时候可能不想要打扰别人。

如何接收 Webmention?

显然,你需要一个 Webmention 接收器。如果你的博客是用 WordPress 构建的,前文提到的 插件 已经具备了接收 Webmention 的功能。如果是其他博客软件,可能要考虑使用第三方 Webmention 接收器,或者自己部署接收器了。

使用 webmention.io

最简单也是最常用的第三方 Webmention 接收服务是 webmention.io ,我也在使用。这个服务是免费且开源的,所以还算值得信赖。要使用他们的服务,你首先需要配置 IndieAuth ,也就是表明你是这个网站的主人,怎么做到呢?

在网站的 <head> 添加:

<link rel="me" href="https://github.com/alice">
<link rel="me" href="https://twitter.com/alice">
<link rel="me" href="mailto:alice@mail.com">

alice 改成你自己的用户名。这个标记的意思是,这个网站的主人有 GitHub、Twitter 和电子邮箱账号,用户名或地址如上。接下来,确保你的 Twitter 账号页面或 GitHub 账号资料里有链接到这个网站。这样一来,你就证明了自己拥有这些账号,IndieAuth 也就允许你用这些账号登录。你不必配置全部三个账号,只需要一个有效的登录方式。如果是邮箱的话,IndieAuth 就会给你发送一封电子邮件确认登录。

你还可以把 <link> 写成 <a> 标签,如果网页里已经有这些账号页面的链接了,只需要给 <a> 加上 rel="me" 即可。这种标记方式叫作 microformats,后文还会提到。

配置好 IndieAuth 之后,在 webmention.io 输入域名登录,接下来会进入仪表盘。现在,你需要在 <head> 里面指定 webmention.io 作为接收器,添加这段内容:

<head>
 <link rel=webmention href=https://webmention.io/你的域名/webmention>
</head>

接下来 webmention.io 就能够帮你接收 Webmention 了。这个服务是 Aaron Parecki 提供的,如果你觉得不错,可以去支持他!他还是 IndieWebCamp 的创始人和 OAuth 工作组的编辑者。

好了,现在你能接收 Webmention 了,数据存放在 webmention.io 的服务器里,要怎么查看接收到的 Webmention 呢?有这样几种方式:

  1. 仪表盘 查看最近的 Webmention。
  2. 设置 获取 RSS/Atom 订阅源,使用 RSS 阅读器查看。
  3. 配置 Webhook ,在有新 Webmention 时向指定 URL 发送 HTTP 请求,可以配合 Barkntfy 推送到你的手机上。
  4. 通过 API 获取数据。

如果你想通过 API 把 Webmention 展示在自己的网站上,最简单的方法是使用 webmention.js 。把 JavaScript 文件放在自己的网站上之后,在需要显示 Webmention 的地方添加:

<div id="webmentions"></div>
<script src="/path/to/webmention.min.js" async></script>

具体的使用方式见项目文档。

如果能力允许,可以自己编写渲染逻辑,毕竟 API 已经有了。

自托管 Webmention 接收端

webmention.io 很好用,配合 webmention.js,甚至不需要自己写一行代码,不需要部署任何服务,就能接收并展示 Webmention,这对大部分人来说已经够了。不过,如果你很在乎自己的数据,并希望尽可能少依赖第三方服务,那么你大概要自己部署接收端了。

在可以自己部署的 Webmention 接收端中,使用者比较多的是 Horst Gutmann 维护的 webmentiond ,用 Go 编写,存储基于 SQLite,简单轻量。此外还有用 Rust 编写的 WesleyAC/webmention-receiver 和用 Python 编写的 capjamesg/webmention-receiver

对于重复造轮子这件事,我们国人也不输(奇怪的比较)。 Chlorine 不久前用 Rust 写了一个 Webmention 接收端,名为 CircleAt ,而我也用一门 Lisp 方言开发了 Weepinbell ,最近才发布 v0.1.0 版本。不过我不建议你用 Weepinbell,因为还没有正式投入使用,可能存在一些问题,进入稳定版本之后我会再发一篇文章的。当然,如果你愿意贡献的话,我将感激不尽。

在开始之前,我得警告读者,Webmention 作为一门小众技术标准,使用者很少,自托管接收器的更少,如果遇到问题,可能很难找到解决方案和能够提供帮助的人(虽然我很欢迎有人来问我啦)。如果对自己的技术没什么信心,webmention.io 已经很好了,比能够自托管的选项都要成熟不少。

不过剩下的我也没什么好讲的了,如果你决定自托管,那就选择一个自己觉得不错的接收端软件部署到服务器上。至于如何查看接收到的 Webmention 和如何展示,不同的接收端都有实现细节上的不同,这里略过。

如何制作一个 Webmention 表单?

还记得我们说发送 Webmention 就是发送一个 POST 请求吗?想想还有什么是 POST 请求?一个网页上能够发送 POST 请求的最常见的元素是什么?

没错,就是表单。

<form action="https://alice.blog/webmenion" method="POST">
 <input type="url" name="source">
 <input type="hidden" name="target" value="{{ .Permalink }}">
 <button type="submit">发送</button>
</form>

其中:

  • action 是 POST 请求要发送到的地方,这里填写你的 Webmention 端点
  • <input name="source"> 是用户输入的,对方文章的 URL
  • <input name="target"> 是你这篇文章的 URL,一般由程序自动生成;这里写的 {{ .Permalink }} 是 Hugo 的页面永久链接,你应该把它替换成你的博客软件的变量。

你需要改的就只是 action 的地址和 <input name="target">value,不需要写 JavaScript。不过,用户点击提交之后会被直接传送到 action 所指向的地址,如果你的 Webmention 接收端只返回 JSON,用户可能就不清楚到底有没有提交成功,还是有些不太友好的。如果你愿意的话,可以自己用 JavaScript 处理返回的数据,制作直观的提示,比如「发送成功」之类的。

放一个这样的表单在网页上并说明清楚作用,即便是不了解 Webmention 的访客也懂得如何使用。

让 Webmention 生动起来:microformats

microformats 是一系列开放数据格式,建立在已被广泛采用的标准之上(比如 HTML)。它并不是和 Webmention 强绑定的,实际上 Webmention 标准完全没有提到过 microformats,只不过 IndieWeb 上的人很喜欢把它和 Webmention 一起用。说起来,应该是 webmention.io 的维护者 Aaron Parecki 带的头,webmention.io 默认支持解析 microformats。

由于没有 microformats 也不影响使用 Webmention,如果你不感兴趣的话,可以跳过这一节。

microformats 是什么?

回答这个问题之前,先要了解它解决了什么问题。

网页可以是任何东西,可以是 Web 应用(比如 Notion 这样的在线笔记),可以是小工具(比如计算器、格式化工具、视频下载器),可以是相册,也可以是一篇文章,可以是日历。同一张网页也可能包含各种各样的东西,一篇博客文章中有页面标题、主体内容、标签、评分、回复等等。

网页的具体结构也是不确定的,为了实现不同的排版和效果,不同网站的 HTML 结构完全不同。有的可能是这样的:

<main id="container">
 <h1 id="post-title">Title</h1>
 <article id="post-content">
 <p>Content</p>
 <!-- ... -->
 <p>Author is ..., follow me on <a href="https://mastodon.social/@author">Mastodon</a></p>
 </article>
 <div id="comment">
 <!-- ... -->
 </div>
</main>

也有可能是这样的:

<main id="container" class="flex justify-center items-center my-10 mx-auto p-4">
 <header class="max-h-lg shadow-lg">
 <div class="rd overflow-hidden">
 <img src="/posts/xxx/banner.jpg" class="block">
 </div>
 <h1 class="font-extrabold text-3xl">Title</h1>
 <p class="font-semibold text-xl">Subtitle</p>
 </header>
 <article class="text-md leading-loose prose">
 <div id="post-content">
 <!-- ... -->
 </div>
 <div id="post-endnotes">
 <ul>
 <!-- ... -->
 </ul>
 </div>
 <div id="author-info" class="bg-white px-4 py-6 my-2 shadow">
 <p>Author Name</p>
 <p>Description</p>
 <a href="https://mastodon.social/@author">Mastodon</a>
 </div>
 </article>
 <footer>
 <!-- ... -->
 </footer>
</main>

偶尔还能见到一些莫名其妙的 HTML 结构,比如我就见过这样的:

<div id="scroll-body">
 <div id="contant-wrap-wrap">
 <div id="content-wrap">
 <div id="main-content">
 <div id="page-content">
 <h1 id="page-title">...</h1>
 <p>...</p>
 </div>
 </div>
 </div>
 </div>
</div>

简直有大病。 无论如何,尽管它们的结构不同,但包含的内容是相似的,换句话说,数据结构是相似的。他们都有可能是一篇文章,包含页面标题、内容、作者信息等等,这篇内容同时还有可能是另一篇内容的回应、点赞或转发。

被浏览器渲染为图形过后,人读起来会理所应当地觉得这些就是相同的数据,但在机器看来就难了。HTML 结构有无限种可能,要怎么让机器快速且准确地判断某个网页属于什么类型的内容,它包含了哪些结构化的数据?

其实已经有一些方案被提出了,比如被广泛使用的 Open Graph 协议 。网页只要按照标准,添加一些 <meta> 标签,就可以使其成为一个数据对象。Facebook 等社交媒体,在遇到这些链接的时候,会爬取 Open Graph 的内容,生成更具视觉效果的卡片。

以下是 Open Graph 的一个例子:

<meta property="og:title" content="The Rock" />
<meta property="og:type" content="video.movie" />
<meta property="og:url" content="https://www.imdb.com/title/tt0117500/" />
<meta property="og:image" content="https://ia.media-imdb.com/images/rock.jpg" />

microformats 是类似的东西,但他没有规定新的 <meta> 标签类型,也不像另一个标准 JSON-LD 一样要求插入一大段包含 JSON 的 <script> 标签。它就建立在最基础的 HTML 之上。

以下是用 microformats 标记的一篇博客文章示例。

<article class="h-entry">
 <h1 class="p-name">Microformats are amazing</h1>
 <p>Published by <a class="p-author h-card" href="http://example.com">W. Developer</a>
 on <time class="dt-published" datetime="2013-06-13 12:00:00">13<sup>th</sup> June 2013</time></p>

 <p class="p-summary">In which I extoll the virtues of using microformats.</p>

 <div class="e-content">
 <p>Blah blah blah</p>
 </div>
</article>

注意到 class 的值了吗?p-name 标记了这篇博客文章的名字,p-author 标记了作者,p-summary 标记了这篇文章的总结摘要,e-content 则是这篇文章的内容。最外层的 h-entry,标记这是一个条目,博客文章、社交媒体发帖和维基页面等等,都可以用这个类名标记。

要实现 microformats 标准,你不需要在其他地方添加任何别的格式的数据,只需要在原有的 HTML 上操作就好了。刚刚我们看到的是 h-entry 格式,用来标记发布在万维网上的内容,除此之外,还有用来标记人和组织的 h-card ,用来标记日历和事件的 h-event 等等。

我们刚才用来配置 IndieAuth 的 <link rel="me"> 标签,也是一个 microformats 格式 。它更常见的用法是直接标记在 <a> 标签上:

<a href="https://github.com/tantek" rel="me">@t</a>
<a href="https://tantek.com/" rel="me">https://tantek.com/</a>

rel="me" 本身不是用来配置 IndieAuth 的,只是标记网站站长的其他网页链接的一种方式。IndieAuth 只是使用了 microformats 的这个格式而已。

总而言之,microformats 是名副其实的「微格式」,不需要对网站结构做出太大改动,就可以标记结构化的数据。

和 Webmention 的关系

我猜测除了 webmention.io 之外,没有太多接收端实现了解析 microformats 的能力(不过, Weepinbell 可以!)。最初 Webmention 仅仅是「A 网页提及了 B 网页」的通知,并没有什么复杂的分类,但 microformats 的使用让一切都不简单了起来。

一般来说,相互提及的网页都是博客文章,文章是用 microformats 的 h-entry 标记的。这个格式除了标记基本的元信息之外,还有这样的属性:

  • u-in-reply-to:这个 h-entry 回复了哪篇文章
  • p-rsvp:这个 h-entry 是对什么的 RSVP,即表明自己是否要参与某次聚会、活动或事件
  • u-like-of:这个 h-entry 是对哪篇文章的喜欢(点赞)
  • u-repost-of:这个 h-entry 是对哪篇文章的转发

比如,Bob 回应了 Alice 的文章,它在文档开头提及了 Alice 的文章,它可能是这样写的:

<div class="h-entry">
 <h1 class="p-name">对 Alice 文章的回复</h1>
 <div class="e-content">
 <p>前几天我在 Alice 的博客上读到了
 <a class="u-in-reply-to" href="https://alice.blog/">
 一篇有趣的文章
 </a>,
 我很受启发。</p>
 <p>对这个问题,我也有相似的思考。</p>
 <!-- ... -->
 </div>
</div>

你注意到了吗?Bob 把文章中提及的链接标记为 u-in-reply-to。如此一来,这篇文章在能够解析 microformats 的程序看来,就是对 Alice 文章的一篇回应。如果是 u-like-of,就是对那篇文章的点赞,u-repost-of 就是转发。

至于 p-rsvp,我觉得这个设计有点大病——真的会有人写网页公开发 RSVP 吗?这种东西难道不应该私下交流吗?无论如何,它是存在的,你可以用 microformats 标记 RSVP,也意味着你可以用 Webmention 来发 RSVP 通知他人你是否决定出席某次活动。

具体而言,Webmention 和 microformats 这两个标准是这样一起使用的:支持解析 microformats 的 Webmention 接收端,在收到请求时,除了做最基本的检查之外,还会解析 source 网页中的 microformats,如果有的话,就一起保存下来。

IndieWeb 上的居民会根据 h-entry 是否包含 u-in-reply-to 等属性,决定展示 Webmention 的方式。如果有 u-in-reply-to,就显示为回复或评论;如果有 u-like-of,就显示为点赞;如果有 u-repost-of,就显示为转发。以下是 Barnaby Walters 的网站实现这一展示方式的例子。

Barnaby 的 网站

如果都没有,那就当作普通的提及,也就是最原始的 Webmention。

在 Webmention 中显示自己的名字和头像

上一小节解释了 h-entry 格式,用来标记内容。用来标记作者时,可以用到 h-card 格式。

<a class="h-card" href="https://tantek.com/">Tantek Çelik</a>,

<span class="h-card">
 <a class="p-name p-org u-url" href="https://microformats.org/">microformats.org</a>
</span>

h-card 本来是独立的,用来标记人和组织的格式,但它也可以作为 h-entry 的子属性 p-author,放在 h-entry 里,标记这个内容的作者。

<div class="h-entry">
 <div class="p-author h-card">
 <img src="https://aaronpk.com/images/aaronpk.jpg" class="u-photo" width="40">
 <a href="https://aaronpk.com/" class="u-url p-name">Aaron Parecki</a>
 </div>
</div>

这样,其他软件在解析 microformats 的时候,就能发现你的名字和头像了。一般来说也会展示在 Webmention 里。

同步来自联邦宇宙的提及

你可能怀疑会不会真的有人这样用 microformats,给链接标记 u-in-reply-to 这样的类名总归有点麻烦,大部分时候我们只是简单提及一篇文章而已,不见得就是对那篇文章的直接回应,或者说点赞和转发。这些东西更像是社交媒体上会用到的。

嗯?社交媒体?

既然我们在讨论开放的标准,那就不得不提及开放且去中心化的社交媒体了。是的,我说的就是 联邦宇宙 。有没有办法把来自联邦宇宙的点赞、回复和转发,以 Webmention 的方式同步到网站上呢?

答案是使用 Brid.gy ,这个服务允许你用 Mastodon 账号(以及其他兼容 Mastodon API 的联邦宇宙软件)登录,并将社交媒体账号与你的网站桥接起来。它做的事情很简单:

  1. 每隔一段时间扫描你的社交媒体时间线,以及其他人与你的互动。
  2. 如果发现你的某篇帖文包含你的网站上的 URL,而又有人点赞、回复或转发了这篇帖文,那么就视作这个人点赞、回复或转发了你网站上对应的这篇文章。
  3. 创建一个占位 HTML 页面,包含用 microformats 标记的 h-entry,以这个占位页面作为 source,给你网站上的页面发送 Webmention。(如果你直接访问这个占位页面,会被跳转到实际的联邦宇宙帖子,但 Webmention 接收器会把这个占位页面视作真实的 source 地址)

如此一来,你的网站就通过 Webmention 和联邦宇宙互通了。

Bridgy 的后台,列出了联邦宇宙上的互动,其中的一些已经作为 Webmention 发送

我的网站就是这样做的,拉到页面最下方,你就能看到来自联邦宇宙的点赞和回应,不过我还没想好应该如何展示「转发」这个类型的 Webmention。

自己动手写 Webmention 接收端

自己写 Webmention 接收器并不困难!你只需要把 Webmention 文档 中有关接收端的部分读一遍,知道需要注意哪些技术细节12就好了。你只需要提供一个用于接收 Webmention 的端点,至于查询 Webmention 的 API,以及要不要解析和储存 microformats,都可以自己决定。

你完全可以只写一个给自己用的 Webmention 接收端,接收到 Webmention 请求后就通过聊天机器人发送给自己,不做展示也不做过多的处理,甚至不用储存。Webmention 尽管有规定删除和更新 Webmention 的方法(如果 source 有更新或者删除了引用,重新发送一次 Webmention 就应该能够更新),但并没有说一定要储存和展示 Webmention。

你也可以把 Webmention 的功能做得很丰富,用来代替评论系统。无论如何,如果你决定自己写 Webmention 接收端的话,这都取决于你,只要保持最基础的标准就好了。

你也可以看看 Salmention ,比起 Webmention,它只是增加了多级回应的功能。比如 Chris 看到 Bob 回应 Alice 的文章之后,也写了一篇文章回应 Bob,并给 Bob 发送了 Salmention。如果 Alice 的网站也支持 Salmention 的话,Alice 就会收到 Chris 的 Salmention,尽管 Chris 只是直接提及了 Bob。不过,这个拓展标准很少有人用。

资源

相关阅读

工具

最后

希望有更多的人能用上 Webmention,一起愉快地写博客交流。

我写累了,一会儿见吧。


  1. 比如,W3C 规定,为了避免 DoS 攻击,所有的 Webmention 必须异步处理,也就是验证 sourcetarget 都是正确的 URL 之后,应该异步地向 source 发送 GET 请求,检查 source 的内容是否包含 target。 ↩︎

  2. 顺带一提,Webmention 其实不止支持 HTML,还可以提及 JSON 文档和纯文本文档。不过,虽然标准是这么写的,但应该不会有太多接收端很好地支持非 HTML 文档。 ↩︎

稻草人周刊 Vol.71

Voicenotes music cover

Voicenotes

Charlie Puth

《Voicenotes》是 Charlie Puth 的第二张录音室专辑,2018 年的热单《Attention》就出自这张专辑。最近在找几年前的专辑来听,走路和跑步时常听这一张。我很喜欢 Charlie Puth 的风格,不过我缺乏基本的乐理知识,不知道从何说起了。这张专辑里我最喜欢的歌曲其实是第一首《The Way I Am》。

I’ma tell ’em all

I’ma tell ’em all that you could either hate me or love me

But that’s just the way I am

顺带一提,Charlie Puth 的下一张专辑《Whatever’s Clever》在本月底就要发行了。我想在这里引用 Taylor Swift 歌曲里的一句歌词:

Charlie Puth should be a bigger artist。1

就这样,继续看周刊吧。如果不麻烦的话,还请留意这一期的「当下」栏目。


连接

程序员如何沟通?

📜

NetNewsWire (macOS 和 iOS 上的自由且开源的 RSS 阅读器)的开发者 Brent Simmons 尝试向亲友解释他的工作,我很喜欢文中的某些句子:

I know I’m a big nerd and what I do must seem all super nerdy and vague.
我知道我是个大书呆子,我做的事情一定看起来超级书呆子,让人看不明白。

My blog inessential.com (this site) has been going since 1999. I mostly write about Apple nerd stuff. Other Apple nerds read it.
我的博客 inessential.com(这个网站)从 1999 年就开始写了。我主要写 Apple 书呆子相关的东西。其他的 Apple 书呆子会读。

In the Apple nerd world I’m the NetNewsWire guy.
在 Apple 书呆子们的世界里我就是 NetNewsWire 哥。

So I’m a nerd and a computer programmer. But what I am not is a math nerd… I’m usually the worst person at math in any room I’m in. Shock, I know!
对,我是个书呆子,是个程序员。但我绝对不是数学书呆子…… 我在任何地方通常都是那个数学最差的人。令人震惊,对吧!?

好了,对 nerd 这个词的滥用我们看够了。读完之后我突然觉得 Brent 变得很亲切,因为我和他一样,数学非常差劲!虽然有些难以启齿,但仔细想想也没什么好隐瞒的:我的高考数学成绩只有 68 分!Brent 说他没有大学毕业,但就算毕业了,也会得到英语学位,而不是数学学位。

似乎全世界都有这样的误解:程序员一定数学很好,要学编程一定要先学好数学。我记得我的初中老师就问过我:“你学这个编程,难道不需要把数学学好吗?”

他关于软件开发的观点我很喜欢,他认为编程的重点不是数学,而是沟通。首先是程序员和机器的沟通,更重要地,如果你在开发一个 App,也要考虑如何设计软件,让软件更好地和用户沟通。我在学软件工程相关的课程,例如需求工程、软件项目管理和架构设计的时候,也逐渐意识到,软件开发中最重要的过程从来不是写代码,而是沟通。架构设计是一种沟通、需求获取是一种沟通,更别提开发团队内部的沟通和与用户的沟通了——这也是我不担心程序员会被 LLM 取代的原因。

Like any other popular art form, it’s about connecting with humans.
就和其他流行的艺术形式一样,重点在于和人的链接。

我想我比起后端更喜欢前端开发的原因也是如此,前端与用户的链接感会更强。不过从技术上,我还是不太喜欢 JavaScript 生态。记得去年还有过学 Swift 做苹果开发的想法,兴许最近也可以捡起来了,先等水逆过去吧。

擦马桶如何帮我维持学者风范?

📻

听这期博客的前几天才刷过马桶,亮到能反光。于是我一边把洗手间的各个角落刷了一遍,一边把这期播客听了两遍。我最喜欢的句子是:

在网上喷粪不如在现实中擦粪。

树老师说他不焦虑、不想死,是因为每天都要做家务,没空想东想西。整天坐在电脑前从事思考工作的人容易产生这样一种假象,认为一切都在控制当中,然而当生活中的小事出了差错,这种差错很容易降他们击垮,陷入崩溃当中。我也赞同,在网络世界、精神世界、知识世界中待久了,需要关注现实,而且是与自己有着直接关联的现实,而非别人的生活、海另一边的战争、某个无聊的家伙对自己的谩骂。

刚打开这期播客没多久,我就做了一个决定。我找到我的室友,告诉他以后的家务都由我来做。这里要交代一个背景:先前我们是按照时间来划分家务职责的,一个人负责一天,但他老是忘记,我也常因为各种小事跳过自己做家务的那一天;最近改成了按照事务划分,每个人负责一部分,当时厕所划分给了我,与是我立马把马桶擦得锃亮给他打了个样,可惜激励效果欠佳。某天我看不下去,自己动手吸尘拖地(这本该是他负责的工作)。于是我想,比起每天阴湿地暗暗谩骂对方不好好做家务,不如自己把活儿全干了。我告诉他接下来的家务我全包了之后,他还有些尴尬地回应说自己最近太忙了。

我最近也没闲着,野心勃勃地同时推进着两个个人项目,学校那边也有各种琐事,辅导员和某些老师也在推着我做职业规划,焦虑是难免的。正是因此,我才开始比以往更加频繁、细致地做家务,把自己的大脑从想东想西的状态拉出来,开始关注身体、生活环境和秩序。

所以,读者有没有什么瓷砖和木地板的清洁剂推荐呢?

亚当·斯密如何颠覆古典美德?

📻

赵老师的讲话方式指定跟树老师学了不少。

播客前半部分阐述了亚当·斯密(《国富论》的作者,现代经济学之父)的生平和轶闻,后半部分进入正题,讲述亚当·斯密眼中现代社会应有的美德,与古典美德有何不同,以及他对于古典美德的观点。

美德在拉丁语中写作 virtus,也就是如今英语里的 virtue,这个词的词根 vir 意思是「男人」,拉丁语中美德的本意是 manliness,可以译作「男子气概」。古希腊语中的美德 ἀρετή 也是类似的意思。谈到古典美德,一般会说到柏拉图的四个枢德,即节制、审慎、勇气和正义2,其中勇气是最重要的。相对的,温柔等特质在古典语境下通常与女性联系起来。简单来说,在古希腊人和古罗马人看来,最勇猛的战士就是最具美德的人。

亚当·斯密认为,古典美德所强调的男子气概是野蛮的、原始的、落后的,现代社会所需要的美德应该是具有同理心(sympathy)的美德,即设身处地为他人着想。在古典美德中,美德有高低贵贱之分,而且往往是生来就具有或者没有的,比如贵族的美德就要比奴隶的美德更高;而亚当·斯密认为的美德是人人都能够获得的,是在社会交往中通过反思获得的。简单来说,亚当·斯密的美德是一种更温顺的美德,如果说古典美德实际上是男人的美德,那么亚当·斯密的美德就更接近女人的美德。不过,亚当·斯密所言的同理心其实和大多数人理解的意思也有差异,比如,他认为和商贩交易也是同理心的体现,因为顾客设身处地为对方着想,提供了对方所需要的东西;再比如,他认为富人和快乐的人更容易引发同理心,而不是穷人和悲伤的人。

对于这两种美德,也可以联系到第 42 期周刊引用的 古典审美与现代审丑 。在我看来,古典美德在如今的视角下更接近于自我要求,而强调同理心的现代美德更接近于社会要求。个体可以要求自身节制,比如节制饮食,但在社会层面,应该对肥胖者有同理心,不去责备他们缺乏节制的美德。3

锻炼是为了什么?

📻

树老师在这期运动闲聊播客里分享了她母亲的锻炼心态,她认为她母亲的心态要比她自己和许多年轻人都健康不少。她的母亲在过去的几十年人生里都没有任何运动习惯,直到年过五十,发现身体正在明显地衰老、恶化,才开始为了维持生命而运动。她锻炼的目的是为了对抗衰老和掌控身体,是为了锻炼神经系统调动身体部位的能力,每天能动起来她就很快乐了。

对比另一种锻炼心态,追求进步、对比和竞争的锻炼,「快乐老女人」的锻炼心态要更健康。前者容易陷入这样的窘境:一旦做不到进步,或者进步不够明显,锻炼的乐趣和动力就会大大减少;一旦有了对比,锻炼的目的就不再是满足自己身体的需求,而是为了迎合别人的审视;一旦有了竞争,人就会感到焦虑。这种锻炼心态甚至成为了门槛,让没有锻炼习惯的人认为,自己要是没有运动天赋,就不该开始运动。实际上,每天都让自己动起来这个事实本身就值得骄傲,就像自己又活了一天一样值得庆祝。

关于锻炼的目的,我的观点可以引用 Steven Pressfield 的《 一生之敌 》中的一句话来阐释:除了爱,出于任何其他原因而从事艺术创作,都是卖淫。 把这句话的「从事艺术创作」换成「锻炼」,就是我的观点,而且我觉得这句话用在「身体」相关的话题上,显得更有说服力。

锻炼是为了保证身体的机能,提升神经适应性,保证自己的身体能做到它应该做到的事情。简单来说,是为了健康,而对健康的追求就是对生命的热爱——我那个经常跑健身房,买五花八门运动补剂,没事就对着镜子拗造型的室友,还对我这个目的颇有微词。 我向来很欣赏热爱力量训练并关注身体变化的人,不过我欣赏的是能从训练和身体变化本身得到满足的人。

更准确地说,我欣赏所有好好为自己和自己热爱的事情活着的人,在我看来,这才是生命力的体现,而生命本身就是意义。

星群

GRAM

Zed 的分支,托管在 Codeberg 上,移除了服务条款和 AI 功能,是纯粹的自由软件,也是纯粹的编辑器。Zed 和 GRAM 是用 Rust 语言编写的 VS Code 替代品,换用 Neovim 之前我一直在使用 Zed,至少在 macOS 上启动速度很快,界面也很干净,是我很喜欢的用户界面风格。

不过 Zed 并不是纯粹的自由软件,因为它有 服务条款 ,还有付费的 AI 功能,是开源的商业软件。最近 Hacker News 上还有有关 Zed 的小 讨论 ,因为 Zed 在服务条款里增加了年龄要求,不过并不是威胁隐私的年龄「验证」,相关内容见 我的帖文 。尽管本体是 GPL 协议,但项目的 README 下赫然写着:

Zed is developed by Zed Industries, Inc., a for-profit company.

那就不能指望挣钱的企业抛弃讨厌的 AI 功能了。GPL 协议的好处就是自由,可以随意更改和分发软件,于是一个移除了服务条款和 AI 功能的 Zed 编辑器就诞生了!也不知道我这个 Neovim 用户在激动什么。

如果你在找 VS Code 替代品4,GRAM 值得一试。

访问: GRAMCodeberg 仓库

blogtato

用 Rust 编写,基于命令行的 RSS 阅读器,无需订阅,本地优先,而且支持使用 Git 同步数据。不过说是阅读器,其实更像是 RSS 更新提示器。这对我来说更好,我一直不太喜欢直接在 NetNewsWire 里阅读文章,更喜欢到源网站上阅读。

blogtato 的操作很简单:

# 添加 RSS/Atom 源,blogtato 会自动搜寻订阅源的具体地址
blog feed add https://www.geedea.pro/

# 同步更新,除非执行这个命令,否则 blogtato 不会发送网络请求
# 也不会占用除存除外的任何系统资源
blog sync

# 查看更新
blog

# 在默认浏览器打开编号为 x 的文章
blog x open

还提供了一些好用的筛选和分组功能:

# 按日期、周或订阅源分组
blog /d
blog /w
blog /f

# 合并分组
blog /d /f

# 按照订阅源筛选
blog @hn

# 按照阅读状态筛选
blog .unread
blog .read
blog .all

# 按时间筛选
blog 1w..
blog 3m..1m
blog /d 2w..1w

# 合并筛选和分组
blog @hn .unread /d

blogtato 原生支持 Git 同步,采用无冲突的设计,同步数据时无需担忧 Git 冲突,数据全部以 JSONL5 存储在本地。

我很喜欢 blogtato 的设计理念,我一两个月前也有用命令行做一个 RSS 阅读器或者书签管理器的想法,但一直没有去实现,没想到这几天就在 Hacker News 上看到有人做出来了。可惜目前还不支持导入和导出 OPML 格式,于是我跟作者提了 Issue ,很快收到了回复。对方表示写一个 Shell 脚本导入也不难(毕竟是命令行工具),但其实项目里已经有解析 XML 的依赖项了,所以作为 blogtato 本身的功能加上也没有什么成本,他说他会看看。很期待更新!

我前几天还在联邦宇宙上吐槽,有了 AeroSpaceVimium 插件之后,我的日常操作都可以用 Vim 键位完成,可惜每次查看 RSS 更新的时候,都苦于 NetNewsWire 不支持用 jk 上下移动。blogtato 至少能保证我的手不用离开键盘。

访问: kantord/blogtato

当下

不出意外的话,这应该是《稻草人周刊》里最后一次出现「当下」这个栏目,原因是我正在计划每月更新的新闻报,也就是「Newsletter」,名字暂时还没有定下来。新闻报中计划包含这些内容:

  1. 过去一个月里,自认为写得不错的博客文章(其实《稻草人周刊》也有「回拨」这个栏目,原本打算每个月写一次,但总是忘记,干脆放到月刊里)
  2. 正在进行或已经完成的项目,情况以及进度回报(我对「项目」的定义很广泛,一般来说是软件项目,也可以是目标明确且耗时较长的大任务,比如「把家里的所有塑料制品都替换掉」)
  3. 书影音游回顾,包含这个月看的剧、写过的书评合集和完整听过的专辑等(也希望能借此治好我的电子阳痿吧)

这意味着《稻草人周刊》会更关注于分享我每周读到的有趣文章、听过的觉得有启发的播客、发现的觉得不错的项目。如果你关心我最近做了什么事情,可以等待新的月刊,预计会在四月初发布第一期;或者,你可以在 联邦宇宙 上关注我,可以用 Mastodon、Misskey、Pleroma 等联邦宇宙软件与我社交。

切片

  • 状态低迷、注意力涣散和效率低下的状态持续挺久了,春节结束回到家也不见好转,遂求助玄学,盘了半天月运势和周运势也没看出个名堂,直到突然被提醒天象:从二月底开始水星逆行了,直到三月 20 日结束。

    水逆啊,那不稀奇了,放心躺吧。

    双子座加月升处女已经被水逆折磨习惯了

  • 夜里睡不着,爬起来看手机,很奇怪,根据我的经验,刷信息流实际上能让我更容易入睡,因为大脑会很快感到疲惫。疲惫之前,看到了 Minecraft 26.1 版本的更新,给所有生物都增加了幼年形态,很可爱。

    还有史诗级的更新,命名牌可以用纸和铁粒(以及金粒和铜粒)合成了!钓鱼和开箱子找命名牌的时代结束了!虽然是很好的更新,但总觉得有些失落——原来现在 Minecraft 里的命名牌已经不是稀奇的事物了啊。

    有些怀念以前的游戏时光,好久没有畅快地和朋友玩一下午 Minecraft 或者饥荒了。想拉拢几个人来,搭一个 Minecraft 服务器,也不知道能不能找到人,或者说自己能不能抽出时间。

    周刊的最后,给各位看看我几年前自己画的 Minecraft 皮肤吧。

    原来那个时候我就开始用考拉当身份标识了吗?

    除了 Vanila 和 Migrator,其他披风我都不知道怎么来的


  1. 出自《The Tortured Poets’ Department》 ↩︎

  2. 参见: 美德 - Wikipedia  ↩︎

  3. 相关博客文章:《 关于肥胖的杂谈 》 ↩︎

  4. 是时候抛弃这个基于 Electron 的软件,停止用浏览器编辑代码了 ↩︎

  5. JSONL 是 JSON Lines 的意思,是每行都是一个有效的 JSON 对象的存储格式,可以理解为把一个 JSON 数组写入文件,但是没有前后的 []。 ↩︎

极客死亡计划书 V

在周末打开终端,cd 进极客死亡计划的项目目录,然后打开 Neovim 开始在自己的代码里到处晃悠,删删改改,对我而言是很放松的体验。我时常在浏览器里注意到某个网页元素,回忆起源代码的位置,立马打开编辑器看看有没有需要去毛球(lint)的地方。这种感觉,其实很像是园丁在自己的花园里闲逛,做些简单的修枝剪叶吧?

总之,某个寻常的周末,我冲了一杯非常好喝的瑰夏,在书桌前坐下,开始在代码之间漫游,发觉修枝剪叶的工作都做完了,此时,一个积压在脑后已久的想法突然涌现。我拿起铲子,盯着其中两个花圃,兴致勃勃地说道:“好,那就开始铲土,重新想象园林设计吧!”——这两个花圃就是 前不久 才被移植过的「议论」和「散文」。是的,我把他们又合并了。


用词说明

为了避免误解,我们先理清一些术语。

下文的「分区」指 Hugo 的 section ,也就是顶层的内容目录,比如 /posts 下有 /posts/xxx/post/yyy 等页面,这个 /posts 就是一个分区。

「分类」是指 Hugo 的 taxonomy ,本站有两种分类,一是「 文章系列 」,二是「 标签 」。不过,出于一些历史原因(其实就是我懒得改),「文章系列」这个分类的代码写作 categories,本意是分类。


命途多舛的极客死亡大地

最初,极客死亡计划上只有一个内容分区,即 /posts 分区,所有的文章都放在这下面,其他的页面是如 /about/linkroll 这样的顶层页面。那是段纯真简单的时光,直到某个名叫 Eltrac 的疯子突然决定要把自己写的垃圾小说都搬过来,于是网站上就多了一个 /fictions 分区。

骇人听闻的事件没有停止,这个疯子决定要给自己读过的每一本书在网站上留一个位置。他先对 稻草人 下了手,心狠手辣地把「书评」分区从周刊手里夺走了,把那些残缺的短评放进了他引以为傲的、单独的 Markdown 文件里,大喊着「原子性!」「双向链接!」的口号,使用名为短代码的邪恶武器将书评嵌入了稻草人的身体里,随后又在他飘忽不定的思绪影响下,把那短代码从整个奥兹国代码库里剥去。由于操作习惯拙劣,Git 历史的可读性极低,他的罪行也就没有留下证据,那些被他摧残的文件都改了名,或者进了垃圾桶,连 git blame 的机会都没有!

简而言之,极客死亡计划又多了一个 /library 分区,纯真简单的小骷髅就这么被知识玷污了,眼神里失去了光!

这便是「议叙文大分裂事变」的历史背景,在那之后,Eltrac 短暂地休息了一会儿,可没过多久,他便又舞起键盘,对网站历史最悠久的古迹下了狠手。一向忠心耿耿 /posts 分区被他无情地斩成两半,有超过半数的页面作为 /essays 分区的新臣民被分离了出去,就这样背井离乡。大魔头对此的辩解是:

你也可以理解为,我把文章分成了 T 和 F 两部分(仔细想想,其实更像是 J 和 P 的两部分,判断和感知的区分)。做这个拆分是因为,我发现自己的 T 和 F 可能会在某个时间段宕机其中一个,把这个分开能让我更安心地在情绪崩溃时发疯、在好奇心超负荷运转时写一写没人关心的新话题。

——《 稻草人周刊 Vol.57

何其可悲!何其可悲!仅仅是因为他自己沉迷于愚蠢的大众心理学理论(MBTI),就祸害了全体网站公民的身心健康!更不用说,此举损人不利己,如今大魔头每次在写作时,都会纠结要把他拙劣的文字放在哪边,/essays/posts 说起来有别,但实际上本是同源,都是他那扭曲头脑不能停止转动的产物。不过,我们也不要太苛责这个敌人,毕竟他自己也尝到了苦头,那便是永远不能停止分类和修整的痛苦,如西西弗斯一般永世不能停下!别听那个叫加缪的老头儿乱说,天天推石头可不幸福!

终于,感谢上天的宽恕,误入迷途的魔头 Eltrac 得到了分类之神 cartlE 的指点。cartlE 刚莅临网站,就立刻发觉稻草人不应与 /posts 的子民待在一起。看着 Eltrac 在代码里给稻草人周刊写下的各种特例,神明 cartlE 皱起了眉头,发出疑问:“你是在解鸡兔同笼问题吗?把鸡放鸡笼里,兔放兔笼里不行吗?”

于是,/posts/weekly 从极客死亡计划的土地上消失了,取而代之的,是新成立的 /weekly。天下苦稻草人周刊久矣!为了恕罪,也向 cartlE 神明展示自己的悔意,Eltrac 立马给稻草人修好了 黄砖路 ,供他自由通行。

很快,高明的 cartlE 发现了 /posts/essays 之间的裂谷,便命信息与沟通之神墨丘利去唤来双子座,让它用蟒蛇架起桥梁,把两块大陆合二为一了。终于,失散已久的家人又重聚了,现在 /posts/essays 不再分裂,他们有了共同的新名字——/article。墨丘利还提议,尽管两个大陆合二为一,但旧的居民仍然保留旧的地址,避免书信不能送达,尤其是来自联邦宇宙和万维网提及的消息。1新的居民以 /article/xxx 为地址,而旧的居民仍然保留 /essays/xxx/posts/xxx 的地址。

为了展示自己真的回心转意,大魔王 Eltrac 主动提出,应该把 /fictions 分区更名为 /fiction,因为目前的其他分区分别是 /article /weekly/library,只留一个复数形式的名字有些眨眼。神明 cartlE 听完,满意地离去了,并留下秘籍供 Eltrac 学习良好 Git commit 习惯的艺术。

就这样,暮光闪闪和他的小伙伴们终于让混沌之王无序痛改前非,懂得了友谊的魔力,小马国又恢复到了祥和之中。

等等,你串台到哪儿去了?

ESC to Reality

我的天哪,究竟是什么样的读者才能读懂上面那一大坨自我意识过剩的产物……

让我按下 Escape 键(指切换为 Neovim 的 NORMAL MODE,正常模式),来认真解释一下极客死亡计划目前的内容分类设计。

首先,博客目前只有四个分区:

  1. 文章 :一般来说,我突然想写点什么东西发出来,都会放在这。
  2. 周刊 :每周一更新的刊物,包含这周读到的有趣的文章、听到的播客和发现的不错的项目等等,是我整理和收集信息的地方。
  3. 书目 :我读完一本书都会写书评放在这里。
  4. 虚构 :我写的各类小说。

其他分区几乎都是不言自明的,只有「文章」需要特别说明。正如《命途多舛的极客死亡大地》一节中所述,这个分区原本是 /posts,一开始被称作「议叙」页面,因为它既包含议论文,又包含叙事文和散文。后来,我觉得这两种文体或许需要分开,于是新增了一个 /essays 分区,把议论文都放到这里来。

其实在一开始创建 /essays 分区的时候,我就发现了问题,我很难判断一篇文章的归属,我记得当时我花了一个下午的时间给文章重新分区。这种分区在后来又造就了更多的问题,比如:同一个「文章系列」应不应包含来自两个分区的内容?我有一个文章系列叫《代码炼金术》,这里面可能包含我尝试新技术的经历和感受,重点在于「我做了什么」,写得比较散,应该归为 /posts;而有的时候,我可能发现了一种新的构建软件的方式,比如 用 Lisp 而不是 HTML 写网页 ,这种有深思熟虑且形成可复用的体系的文章,似乎应该归为 /essays。可是,它们都是和写代码相关的,不应该都放到《代码炼金术》这个文章系列里吗?如果一个文章系列里既有议论文,又有记叙文,那不是乱套了吗?

就算不考虑文章系列,确定一篇文章属于 /posts 还是 /essays 也不容易。前者是写「我做了什么」,后者是写「我思考并的出了什么」,实际上有相当一部分文章的写作逻辑是「我做了一件事情,并且借此有了新的思考,产生了新的想法」,那这还要怎么分区?

从盒子里跳出来想问题:给议论文和记叙文分区真的是有必要的吗?

最后我接受了自己的风格就是叙中有议、议中有叙的,于是把两个分区合并到了一起。不过周刊里的东西实在是太杂了,给它单独设立分区会更合理。

Tag, You’re It

你可能会以为,这个前不久还重新装修过 标签文章系列 页面的 Eltrac,又要阴晴不定地把标签和文章系列的其中一个分类给扬掉了,毕竟已经有分区了,再算上这两个分类,网站实际上有三个维度的分类,这太复杂了!

当然,看到「你可能会以为」几个字就知道,我不会这么干。

保留文章系列其实很好理解,保留标签是为了什么?

之前在 Jim Nielsen 的博客上读到过这样的观点:你不需要创建标签,链接就是标签。这里的链接是指在文章中链接到的其他文章,文章之间相互引用所形成的关联,就足够让文章物以类聚了,不需要手动打标签,做额外的分类。这的确是很诱人的想法,所以我给博客做了 视觉化的双向链接图谱 ,然后就把「用链接替代标签」这事抛之脑后了。

不移除标签,首当其中的原因就是,双向链接图谱虽然很酷,但是不方便查询。如果真的要找我写的有关某一主题的文章,到像海一样的双链图谱里去找是低效的,这些文章之间也不一定有超链接关联。

再者,我的博客也不是维基百科。据说从维基百科的某个页面开始,一直点击页面中的第一个超链接前往不同的页面,最终会到达「哲学」这个词条。我添加超链接的习惯不如维基百科编辑者那样专业、审慎和克制,没法做到这么严谨的关联,说实话,也不需要这样严谨。

不过,要说最重要的原因,那还是因为我想要按照主题划分文章。我是个兴趣泛滥者,如果你关注我的博客足够长的时间,就会发现我既谈游戏开发,又谈前端开发,还做命令行工具;我不仅写计算机,还常常观察人类,并写下思考;我不仅对社会学、心理学和经济学这些科学感兴趣,还对文学、哲学和语言学感兴趣,我甚至还探索神秘学,会研究占星和塔罗牌;即便是文学这一个类别,我也会涉猎法国文学、拉美文学、日本推理文学和各种杂七杂八的门类。

五花八门的内容要是不按照主题分类,恐怕读者难以把握脉络,也难以找到自己真正感兴趣的内容。假设一个读者只是因为神秘学而关注我,他就可以去阅读 #神秘学 标签下的内容。标签实际上提供了内容的切片。

此外,并不止是文章,在我的设想里,网站的所有内容都通过标签来归类,而文章系列只是给文章的。如果你打开网站的标签页来看过,就可能发现 #心理学 这个标签下不仅有我写的文章,还有《 被讨厌的勇气 》《 天生不同 》《 如何在黑暗的房间里找到一只猫 》等书籍的书评,实际上小说和周刊也按照标签归类。

网站页面,列出了同一个标签下的所有内容,包括普通的文章、周刊、小说和书籍,这些内容是分开罗列的

#内阻力 标签页的截图,截于 2026 年 3 月 8 日

不过,现实有些残酷,从 GoatCounter 的统计数据来看,几乎就没有访客会点开任何一个标签页面,我设想中的「读者可以按照自己感兴趣的话题查看内容」这个需求用例实际上根本不存在。对我自己而已,我也很少在某天突发奇想「要不要看看自己写过什么和占星有关的文章呢?」,然后点开标签页面查看,对我自己的用处也不大。

所以,为什么要留着标签这个东西呢?管理标签费事费力,为了打标签,每次都要先查看已有的标签,用人眼浏览一遍,看看有没有能对得上的;还要克制住自己新增标签的欲望,避免出现太多只有一两篇文章的孤立标签;更麻烦的是,还要记住各个标签的语义,不能混用。标签也很难体现出层级关系,却在很多场景下不能避免进行细分,比如博客就同时存在「神秘学」「占星」和「塔罗」三个标签,而第一个标签实际上是后两个标签的大类——能不能合并成一个呢?它们真的经常出现。然而,有时候我只是广泛地谈一谈神秘学,比如《 好为人师如何帮我反思现代科学的局限性? 》这篇文章,仅仅是在论述「适当的玄学和迷信为什么是有用的,甚至能达到理性的科学知识达不到的效果」这个观点;有时候,我又是在具体地谈 塔罗牌里某张牌的含义 ,这就没必要打上更宽泛的「神秘学」标签了。2

无论以上这些问题的答案如何,都不能发现,标签的存在本身就引发了许多管理难题,而它带来的实际价值却非常少。很明显,我应该把它删掉。

可我偏不。

为什么?答案其实相当不理性,但也相当具有说服力:我不想把它删掉。我想我可能是喜欢标签体系带来的秩序感,也需要建立和维护秩序的感觉。另外,我总觉得,网站的分类就和图书馆一样,大部分找书的人其实都是找到具体书籍的编号之后,直奔书本所在地,但也不能忽视,有很少的一部分人愿意在某个主题的书架附近漫游,发现从未见过的书本。所以,标签可能大多数时候派不上用场,但对我而言,是不能没有的。

最后

所以,这大概就是新的秩序了。

《极客死亡计划书》系列文章的最后,还是要给下一期留个引子。我正在思考博客的交互设计。一两个月前就有关注我的读者可能有印象,以往博客文章的底部有一个「心脏」按钮,将鼠标移上去,显示的文字是「为这篇文章献上心脏」,点击之后,心脏图标会变红,文字会变为「你已经献上心脏了」。这其实是点赞按钮,任何人都可以点击它,让点赞数量增加 1。并且,读者点赞之后,页面的最下方会出现一整排「已经收集到的心脏」,有多少点赞数就有多少心脏。我印象最深的是,有一篇文章的点赞数量超过了 20,当时页面下面非常壮观。

图片最上方是三个按钮,一个评论按钮、Webmention 按钮和「献上心脏」按钮。心脏的数量是 26,页面下方摆满了人体器官。

旧的文章互动区

可惜,「献上心脏」随着 评论系统的移除 一同消失了,一直没有加回来的原因是,我在纠结如何把本身就有些复杂的新交互区设计得更简洁一些,增加元素不太明智。再者,现在博客本身也会同步来自联邦宇宙的点赞,功能上有些重叠。

不过,我想我大概还是会想办法把「献上心脏」加回来的,因为我的确需要一些实在的反馈,不只是访问统计数据。说实话,现在会在联邦宇宙上点赞和留言的读者不算多,至少我很久没看到点赞的数量超过 10 了。尽管,最健康的态度应该是完全不在乎这些数据,只关注和真实的人建立起的链接,但…… 我想我还需要再思考一下。

以前,网站的标签页面还有一个操作按钮,是一只乌鸦,按下之后会隐藏当前页面下的所有「稻草人周刊」。当时这么设计,是因为稻草人周刊混杂在文章分区内,本身的内容又很杂,容易让人抓不住重点,所以提供了一个筛选功能。当时我还费心设计了动画,把鼠标移动到乌鸦头上,页面里的稻草人周刊链接就会闪烁,点击之后这些链接就会收缩起来,直到消失;再次点击按钮,这些链接又会在入场动画之后出现。由于现在周刊是单独的分区了,没有混杂在文章里,这个按钮也就不再需要了。

这些好的改动,似乎磨灭了一些个性,有些不符合我在《 「极客死亡计划」的设计哲学 》里提到的第八项原则。当时举的例子里,还留下的就只有 404 页面和 Batrick3 了。

这些设计的去留和增改的确需要好好思考一下,不过这就是下期的内容了。

回见!


  1. OK,我知道我有点放飞自我了,所以我暂时脱离精神分裂状态来解释一下,这里说的是:我用 Gemini(双子座)生成了一个 Python(蟒蛇)脚本,让它把两个分区合成一个了,之所以提到墨丘利(Mercury),是因为双子座的守护行星是水星(Mercury)。由于 Webmention 依赖 URL 标识源地址和目标地址,如果改了地址,仅仅作重定向是不够的,会导致旧的 Webmention 失效,除非重新发送一遍,所以就用 Python 脚本给每篇文章都添加了 url 属性,这样就算文件放在 /article/xxx.md 下,URL 也还是 /posts/xxx 或者 /essays/xxx。 ↩︎

  2. 不要建议我用大语言模型自动打标签,如果我不能清楚明白每个标签的具体语义,那做分类还有什么意义?为什么要为了支撑一个已经丧失了意义的事物的存在,而浪费大量的算力? ↩︎

  3. 如果你把页面往上滚动,就会看到一直蝙蝠从页面顶部飞下来,它是「返回顶部按钮」。点击它,Batrick 就会带你上去。 ↩︎

稻草人周刊 Vol.70

The Dark Side of the Moon music cover

The Dark Side of the Moon

Pink Floyd

这是一张上世纪七十年代发行的摇滚专辑,是 Pink Floyd 乐队发行的第八张录音室专辑,名为《月之暗面》。我最喜欢的一首是 Brain Damage(脑损),不过,我建议你一定要从头到尾把专辑听一遍。专辑不长,只有十首歌,一共四十多分钟。这首 Brain Damage 和下一首 Eclipse(日食)连接非常顺滑,让不仔细听的人觉得是同一首歌,整张专辑都用这样的衔接串联了起来,最后以心跳声结束。

And if the dam breaks open many years too soon
如果大坝早了许多年破裂

And if there is no room upon the hill
如果山丘上已经没有位置

And if your head explodes with dark forebodings, too
如果你的头脑也因不详的预感爆开

I’ll see you on the dark side of the moon
我和你会在月之暗面相见

连接

FLOSS 和 FOSS

📜

FLOSS and FOSS by Richard Stallman

如果你不知道这个作者是谁:他开发了 Emacs 编辑器,还编写了 GNU GPL 开源协议,是自由软件基金会的创始人。

本文解释了 FLOSS 和 FOSS 的区别,以及相关术语的政治倾向。简单来说,自由软件社区有两个政治派别:自由软件运动派(free software movement)和开源派(open-source)——这两个概念完全不一样,不应该混淆。自由软件是指可以自由运行、研究、更改,以及分发未更改或更改后副本的软件。开源的概念是后来(1998 年)出现的,一开始是为了避免人们混淆「Free」的意思(可能会被误以为是「免费软件」),不过开源的意思很快发生了改变,与自由软件运动分道扬镳。许多人把开源当成自由软件的商业概念,好让商业公司对自由软件的实用价值(practical benefits)感兴趣,而忽视了「自由」,很快「开源」就变成了强大、可靠软件的代名词,人们对「自由」的关注减少了。

很不幸,我最初接触到的概念就是「开源」,而非「自由软件」。

既然有两派之分,自然也有中立派。中立派使用「自由和开源软件」这个词,即 FOSS(Free and Open-Source Software)。这个词没能解释「Free」的真正含义,所以有人会使用法语或西语单词「Libre」来准确表达「自由」的意思,继而有了 FLOSS(Free/Libre Open-Source Software)这个词。Richard Stallman 在文中表示,自由软件运动的参与者不会使用 FOSS 或 FLOSS,因为这个词把 Free Software 切分开了,把注意力给了 Open-Source;应该使用 Free Software 或者 Libre Software 这个词。

至于我的态度,抛开别的不谈,如果只用言简意赅的程度来评判,我不得不说,「自由软件」是更好的词。此外,FOSS 和 FLOSS 作为缩略词,理解门槛会更高一些,而且中文里还是尽可能少夹杂英文比较好1

被赋予的自由不是自由

📻

播客从女性主义展开,回顾和分析了现代的人权进程,关于「自由选择的权利」。

播客中重要的观点是:被赋予的自由不是自由。一个思想停留在前现代的老妇人,没有自我意识,明明知道自己的丈夫对她不好,甚至恶语相向,却还要求死后跟丈夫葬在一起,并教育子女成为和他一样的受害者。这类人无疑是可怜的,但没有办法被拯救,即便他们在政治层面拥有了自由,他们也无法真正行使自由,甚者,他们根本不觉得自己是自由的,或者完全不会思考「自由」这个概念。

对于受过现代教育的人而言,自由也不见得总是好事。因为自由太广泛,选择的数目太多,人们获得的不是选择自由,而是选择困难。这种选择困难小到购买家电时陷入纠结,难以找到最好的选项;大到面对分叉的人生道路时,因无法在可能性之间做出选择,而荒废宝贵的时间。自由甚至妨碍了人们好好生活。许多人感到焦虑痛苦,就是因为有着无止尽的选择,每天都在为上一个选择而后悔,为下一个选择而纠结。这么看来,陈腐、老旧的生活方式,由于不需要支付选择自由的代价,反而是轻松的人生。

播客并没有给出解决方案,兴许也不会有适用于所有人的解决方案。争取自由(各种意义上的自由)都是要付出代价的,轻松的做法当然是服从长辈、听信权威、享受大科技公司的产品,把选择的代价外包出去,但别忘了,在「自由」和「轻松」之间选择,也需要拥有选择的自由。

永远不要买 .online 域名

📜

作者在 Namecheap 上买了很便宜的 .online 域名,用来做产品的主页,网站上只放了一些介绍,和 App Store 的链接。某天他发现网站被标记为「危险网站」,整个浏览器页面都是红色的,绕过警告强制打开后,发现无法正常访问网站,状态显示 serverHold。排查后发现 DNS 没有解析,原因是这个域名被列进了「安全浏览黑名单」(Safe Browsing blacklist)。

在邮件联系了注册商和注册局之后,作者得知他必须在谷歌搜索控制台(Google Search Console)验证域名所有权,才能申请重审,但这无法做到,因为验证身份需要更新 DNS 记录,而他的域名因为被列入黑名单而根本没有被解析。

所以,教训就是:不要买看起来很奇怪的 TLD。

蠢货相处会变老得更快

📃

这是一篇科学研究,我只读了摘要部分,所以会有疏漏。

简单来说,负面的社会联系(negative social ties)会加速衰老和增加疾病发病率,其中衰老是通过基于 DNA 甲基的衰老生物钟测量的(DNA methylation-based biological aging clocks)。文中把这种负面的社会联系称作 Hassler(纠缠者),并发现研究中 30% 个体都报告他们的社交网络中,至少有一位 Hassler。Hassler 一般占据社交圈的外围,属于弱连接。女性、烟民、不健康者和有着恶劣的童年经历的人,更容易遇到 Hassler,这形成了规律:在社交层面易受攻击(vulnerable)、在健康层面易受病扰的人更容易遇到 Hassler。所以,我猜,如果要尽可能少遇到这类人,应该要变得足够自信和强大才行。

社交网络中每多一个 Hassler,衰老的速度就会增加 1.5%,大概会变老 9 个月。不同的人影响也不同,亲属和非亲属 Hassler 与健康有着有害的联系,而如果伴侣是 Hassler 的话,则没有。如果 Hassler 数量众多,对健康的影响就不只是变老那么简单。

我最近在使用一个 App 分析我锻炼时的「最大摄氧量」,这个 App 会对比同龄人中的平均数据,并计算出我的身体年龄(当然和研究中使用的方法不一样,软件算法也不能和科学研究相提并论)。春节回到家之后,App 计算得出的我的身体情况明显下滑,并表示我正在快速衰老(Aging Quickly),而我刚回到自己家一天,这个数据就开始慢慢回升了。尽管没有太多科学依据,但我觉得这是我远离了一大群 Hassler 导致的,当然还有睡眠的影响,在老家的时候总是睡不好。

星群

互联网电话册

在一个电话界面拨号,就能接通某个个人网站。Internet Phone Book 是每年发布一次的刊物,收录了各种有趣的个人网站,每个网站都有「电话号码」。我是在 Elle 的主页发现这个网站的,他的互联网电话号码是 677

不过我找了半天,也没有找到提交网站的地方,内容兴许都是创始人亲自挑选和收录的吧。

Intert Phone Book: Dial-a-Site

Internet Phone Book 的拨号页面

访问: Internet Phone Book

SplatHash

一个用户生成模糊图片的哈希算法,与 BlurHash 类似,不同的是,这个算法能给任何图片生成固定 16 字节大小的哈希,也可以表示为 22 个字符的 base64 编码字符串,而且,它的解码速度非常快,占用非常小。目前主要提供 Go 语言、TypeScript 和 Python 实现的程序。

这个算法也有缺点,是我的个人观点:生成出来的模糊图片太丑了。

SplatHash 算法结果与 ThumbHash 和 BlurHash 的对比

访问: junevm/splathash

Stop Tahoe Update

Stop Tahoe Update(停止 Tahoe 更新)是由社区维护的项目,帮助不想更新到 macOS Tahoe 的用户留在 Sequoia 等稳定版本上。目前这个项目提供了 Device Management 的配置文件,通过这个配置文件,用户能够推迟 macOS 更新最长 90 天,具体效果是:

  1. 阻止「今晚安装」和「立即安装」的提示。
  2. 阻止系统设置中「有新版本」的数字提示。
  3. 检测并选择性阻止 macOS 系统应用的安装。

我的 Mac Mini 安装了 Tahoe,但我的主力机 MacBook 还是 Sequoia,我也不打算更新。我时常会收到系统更新提示的骚扰,这个项目帮助很大。顺带一提,我觉得这句话我永远也说不够:macOS Tahoe 毁了 Safari!

访问: travisvn/stop-tahoe-update

当下

平淡无奇的日常

本周是待在老家的最后一周,周末就回家了。回去之前除了每天给弟弟补习英语,偶尔出门闲逛,坐在及其不舒服的桌椅上看剧和玩网,以及读读书和写写代码之后,就没干别的了。

唯一的例外是周二出门和高中同学玩剧本杀,两三年没见面,感觉大家都没怎么变。不过,倒是有不少人惊讶我瘦了好多,还挺爽的。剧本是不用带脑子玩的类型,很俗套的家庭纠葛,结婚、争彩礼嫁妆、离婚、分财产和抚养权…… 不过倒是有很多吵架和演戏的情节,非常戏剧化,玩得还算开心。

我真的是个内向者吗?

在老家的几周,每天都想出门闲逛,但又找不到地方去。回家之后,倒是每天都想在家待着,要逼自己多出门走走,取个快递、倒倒垃圾之类的。在家多快乐,走出书房就可以去吧台给自己冲杯咖啡,冰箱里没有被喜好囤积的老人塞满不知道是什么的冻货,每个地方都是干净整洁的。

就决定是你了,口呆花!

Weepinbell 稳步开发中(指周日的时候突然想起来自己打算用 Clojure 写个 Webmention 接收端,这才 cd 进项目目录)。周中把 Webmention 规范 里有关接收端的部分仔细读了一遍,发现这真的是一项非常简单的技术规范,门槛并不高,只是知名度太少,导致普通用户没有开箱即用的选择。

简单到什么程度呢?大概就是发送端只需要向接收端发送一个 POST 请求(只包含 sourcetarget 两项数据,表单格式,不用 JSON),而接收端也只需要处理这一个 POST 请求,检查一下 URL 是否合法、有没有自己引用自己的情况、爬一下 source 看看是不是真的引用了 target。除了规范要求 Webmention 验证应该异步进行之外,就没有太多其他的技术规范了,收到 Webmention 之后要怎么存储、怎么展示、作何处理,都是接收端自己决定。

用 Clojure 编写 Web 应用的体验也很好,可以直接偷 Java 生态的库来用(比如,我验证 URL 的时候就用到了 apache.commons.validator,提取域名的时候用到了 java.net)。由于需要异步编程,还研究了一下 clojure.core.async 库,发现 Cloure 竟然支持和 Go 语言类似的并发模型,有 gochan,没想到偷完 Java 生态,还能偷 Go 的异步编程最佳实践来用,而且能复用编写 Java 和 Go 两门语言的程序的经验,真的很舒服了。

处理 Webmention 很显然需要一个消息队列:把所有待处理的 Webmention 都放在一个 channel 里(在 Go 语言里是c := make(chan type),在 Clojure 里是 (def c (chan))),然后启动一个或多个 goroutinechannel 里取出 Webmention 对象,爬取 source 验证是否包含引用,确认无误之后存入数据库,这些都可以轻松地异步执行。不过 Clojure 里应该是没有 goroutine 这个东西了,不知道底层是不是 Java 那臃肿的 Thread。无论如何,能够直接写 (go ...) 进行并发就已经很好了。

至于存储,我还在思考选用什么数据库。尽管在部署应用时常用 SQLite,但开发时我只用过 MySQL 和 MariaDB。我想,这正好是尝试新技术的机会,而且 Webmention 相互没有关联,数据结构并不复杂,兴许用 NoSQL(非关系型数据库)很合适,正好可以试试 MongoDB。另外的选择是用 SQLite 存储 JSON 数据,或者直接把所有 Webmention 都放在一个 JSON 文件里。这些都是下周要去做的事情了。

最后解释一下为什么项目名字叫 Weepinbell 吧。因为不想起名字,又受 Repokemon 启发,决定选一个宝可梦的名字。一开始想取 Web(网络/蜘蛛网)的意思,选个配色和 Clojure 的蓝绿色调相似的宝可梦,所以看上了滴蛛(Dewpider)。不过本人很讨厌蜘蛛,而且 mention 这个词也完全没体现出来。最后翻来翻去,选择了口呆花(Weepinbell)的名字。因为前两个字母 We 和 Webmention 的前两个字母相同,而后面的 bell 一词意为「铃铛」,可以表示「通知」,而 Webmention 本身的用途就是通知,通知作者他的内容被另一个站点上的内容引用了。

切片

  • “哈!你用浏览器编辑文本。” —— NeoVim 的主要贡献者 TJ DeVries 在 2024 年 VimConf 劝告 (Neo)Vim 用户们不要用这种话说服别人使用 (Neo)Vim。

    可是,VS Code(以及 Cursor 等 VS Code 分支)就是浏览器啊!

    逃离现场。

  • 看到日本网站丑丑的设计反而觉得有点喜欢,大概是因为喜欢多样性,扁平、单色、冷静、克制的设计看太多了,不免有些乏味。( 相关链接 )最近在 IndieWeb 上闲逛的时候,也看到了一些不太主流的设计,我想这是个性的体现,比在主题商店找到的好看模板更吸引人。

  • 想把旧电脑的硬盘拆下来做成移动硬盘,硬盘盒已经买好了,结果我拆电脑的时候被父亲撞见。

    他:电脑留给我用,硬盘我给你买一个。

    我:啊?现在硬盘贵得很……

    他:没事,要好多钱你给我说嘛。

    于是我现在有了一个 2TB 大小的移动 SSD,目前用来备份 iCloud、S3 储存桶和服务器,兴许还会存点音乐和剧什么的吧。感觉可以做一个末日2生存包,存一些音乐和《老友记》全集,应该能维持很长一段时间的 san 值健康。

  • 又买了管道疏通剂,尝试疏通浴室的地漏。用完一整瓶疏通剂之后才发现,水排得慢并不是因为管道堵塞,而是滤网的设计缺陷。说是设计缺陷毫不夸张,因为那个金属滤网上有一个类似盖子的结构,会在安装上之后挡住排水口,只留下非常小的缝隙;而盖子旁边是环形的凹槽,那里会积水,大概是和管道形成了类似连通器的结构,凹槽水满了之后会从盖子的缝隙流到管道里。

    这貌似是给水量不大但有排水需求的场所使用的滤网,能够有效拦截固体避免堵塞,但根本没办法在淋浴时有效排水,房东装这房子的时候是怎么想的?

    室友:能不能把滤网翻过来用?

    如果忽略掉那个朝上的盖子,洗澡时小心不要踩在上面的话,排水就很顺畅了…… 忍了半年的积水问题,竟然是这个原因吗……


  1. 用词 页面已更新。 ↩︎

  2. 指断网。 ↩︎

迷失于图形界面

学校开设了 Python 课程,课程还涉及人工智能相关的内容,但也有相当一部分内容是 Python 程序设计基础。自然地,第一堂课的内容是配置开发环境,自然地,配置开发环境的内容就是安装 PyCharm,并在 PyCharm 的图形界面里找到对应的面板安装要用的依赖项。

一般来说,只要老师没有妨碍我愉快地用我爱用的编辑器(Neovim)写代码,我是不会说什么的。只不过,这位老师之前也教过我数据库的课程,他对于开发工具的态度在我看来非常典型,所以值得一谈。

不解之词

老师在演示如何用 PyCharm 安装依赖时说了这样一句话:

“我看有的人用写代码的方式就能把依赖安装上了,那样好像是会快很多,有些人就是能找到捷径啊!”

尽管我对 Python 生态不熟悉,但根据他的描述,我大概明白他说的是类似 Node.js 的 package.json、Go 语言的 go.mod 和 Maven 的 pom.xml 之类的东西。在项目根目录的一个文件声明项目所需的依赖之后,执行安装命令,就能在当前项目下安装所需的依赖。这实际上是相当常见的实践,有正经工程应用的编程语言都有类似的东西。

在讲授数据库原理及其应用这门课的时候,他要求我们安装的是 Navicat ,一个管理数据库的图形界面工具。我不想用闭源的商业软件(而且我当时的操作系统是 Arch Linux,似乎也没有在 AUR 上找到包),所以选择了 DBeaver 。我记得很清楚,我除了在需要提交作业截图时使用了这个软件,其他时候都是用 MariaDB 自带的命令行工具,手打 SELECTINSERT INTO 等语句,因为我真的不想在层层叠叠的图形界面里找我想要的功能。

我记得有一次交作业我实在不想打开 DBeaver,于是提交了终端截图,结果下一次上课的时候就听到他这样说:

“有的同学直接输入 SQL 语句,都不嫌麻烦的吗?临时输入一两句还好,要输的东西多的时候,就直接在软件的表格里编辑数据表就好了啊。”

那这和用 Excel 有什么区别!

到目前为止,这还只是操作习惯的差异,不足为奇。所以,老师是一个喜欢操作图形界面的人,这不代表他是个差劲的程序员,程序员的能力不体现在工具的选择上,不熟悉命令行工具又怎么了?用 Neovim 和 Emacs 就比用 VS Code 更高尚?用 Postman 就比用 curl 更愚蠢?用集成开发环境也不代表无能啊。

“大家用 pip 安装依赖的时候注意一下啊。”老师好像是突然想起了以往的经历,在讲课时突然插入了这样一句话,“用 pip 安装的第三方库和你的 PyCharm 里的第三方库可能不会互通,就是说你用 pip 安装了第三方库之后,在 PyCharm 里可能用不了,要在 PyCharm 的图形界面里安装依赖。”

我没学过 Python,但确实用 Python 写过不少脚本来帮我自动化处理某些操作,比如批量修改博客的 Markdown 文件格式、替换文本之类的,我也的确遇到过没办法正常加载依赖的问题。当时我只能改变实现方式,避免调用第三方库,一直没明白原因。这么说,我的问题就要从老师那里得到解答了?

没有,他直接往后讲了,什么也没说。他自己大概也不明白为什么 pip 安装的依赖会没办法在 PyCharm 里使用。

求助熟练操作蟒蛇狐狸 之后得知,这是 Python 的 虚拟环境 导致的,即 venv。虚拟环境会将软件包与全局环境的软件包隔离开来,只有在环境内显式声明的软件包才可用。pip 默认把软件包安装到全局环境中,而 PyCharm 会帮忙管理虚拟环境,两者不在同一个环境里操作软件包,所以没办法直接互通。

这就解释清楚了,所以我只需要执行 source 命令进入虚拟环境,然后在虚拟环境里管理依赖和运行 Python 程序就可以了;如果安装了 uv,使用 uv run ,可以直接在项目的环境里执行命令。1

不过,老师看起来教授 Python 也有几年了,为什么会不清楚这个基础概念呢?难道是我误解了老师的……

好了,是时候放下我对教师的假惺惺的尊敬了。

图形界面的魔法

图形界面的优点在于,它屏蔽了很多细节,避免直接和复杂的配置文件和基础工具打交道。这无可厚非,软件设计的一大原则就是「信息屏蔽」,让用户关注更重要的事情。可是,对于程序员来说,这些细节真的应该被屏蔽吗?一个好的程序员不应该了解自己所使用的开发工具是如何运作的吗?

我在英文博客上写过一篇《 Java Development with True IDEA 》,强烈抨击了 IntelliJ IDEA 单边主义,2并把软件开发所需要的 IDEA 重新定义为 Instant(快速)、Deliberate(审慎)、Elegant(优雅)和 Attentive(专心)。

我当时连续使用 IDEA 将近一年的时间,原因在于:没了 IDEA 我就不知道怎么开发 Java 项目了。之所以会这样,是因为 IDEA 给我屏蔽的信息太多了。不操心底层真正发生了什么的后果是,我根本不知道底下发生了什么。当我按下 IDEA 的「运行」按钮时,究竟有什么命令被执行了?我不清楚,以前的我也不关心,我只知道「这个按钮能让我的代码跑起来」,就像魔法一样。

可是,要是有一天魔法失效了呢?当我按下「运行」的时候程序没有被执行,当我按下「同步」按钮的时候依赖没有被同步(IDEA 的设计非常奇怪,有时候「同步」按钮根本不会显示,我必须在一个下拉框里找到某个菜单项才能正常同步依赖),当我的程序没有被正常编译,而我在密密麻麻的界面里找不到相关的配置项时,我要怎么办?

要找到图形界面里的某项配置可真不简单。

首先,我很难用语言描述清楚我要找的是什么,如果描述不清楚,又怎么在搜索引擎里搜索教程呢?其次,就算找到了我要的东西,有人告诉我这个东西要在 Project Strucutre -> Libraries -> More 里面找到,我也要在硕大的图形界面里搜寻这些文字的位置。再次,就算有截图指引,我也有可能因为版本差异而没有办法在图中所示的地方找到我需要的东西。

有人可能会因为熟悉图形界面迷宫而感到自豪吧。我记得我在各种按钮、菜单、面板和输入框之间穿梭,只为了调整项目的编译方式时,有一个不熟悉计算机的朋友突然凑过来看我在做什么,发出了「好厉害」的感叹。可是,究竟为什么要学习一套可有可无的、建立在基础工具之上的抽象,而不去直接使用基础工具呢?

这就好比用 React 开发一个只有一页的网站,而这一页的内容只是几句话和几个链接——那你为什么不能直接写 HTML,而要去操作虚拟 DOM 呢?这层多余的抽象究竟给你带来了什么价值?

我为什么要学习 Postman 的图形界面,等待它花几秒钟的时间启动一个 Chromium 实例渲染一个网页,学习和记忆 URL 输入框 和 HTTP 请求方法的选择框的位置,只为了发一个简单的 HTTP 请求?我为什么不能直接使用 curl?如果我只会用 Postman,我就不知道要怎么在其他的地方发送 HTTP 请求了,而 curl 却被安装在几乎任何一台计算机上,是很多软件的基础工具,无论是日常测试、运维还是开发工作,都能用到。我调试自己写的 API 会用 curl,我测试部署的服务有没有真的跑起来会 curl localhost:<port>,像 Hurl 这类与 HTTP 请求相关的软件也是在 curl 的基础上开发的。

显然,无论选用抽象层次很高的图形界面软件,还是最基础的命令行工具,都有学习的成本。那为什么要学习一套将你与底层隔离开来的抽象,而不是直接学习更接近底层的工具呢?

我承认抽象有存在的价值,但我反对过度且不必要的抽象。

专业与门槛

可能有些幼稚,但我又要举一个大学老师的例子了。不过这个老师本身就很讨人厌,说话爹味很重而且粗俗,还觉得自己和蔼可亲,他项目组里的学生能跑的都跑了。他身上有一个很多爹味浓重的人都有的典型特质:热衷于做「守门人」,强调门槛与专业性,踩在门槛上拉开自己与他人的距离。

他也喜欢使用图形界面,但原因,在我看来,并非是权衡利弊之后的选择,而是对门槛的维护。在我看来,如果你不想接触基础工具,只想要优化开发体验,那么选择简洁直观的图形界面工具也无可厚非,只要这是你自己选的。

设计软件架构常用 UML,即统一建模语言,UML 有各种不同类型的图表,用来表达架构、活动、操作的时间顺序、涉及的模块和对象等等,总之是便于沟通的标准化工具。既然目的是「沟通」,那么只要能准确表达,能让他人理解,就达到了目的。简单来说,只要图画得清晰,没有错误和歧义,能够高效沟通,就是好图。

但是这位老师显然不这么认为。

“有的同学用 Visio 画图,这样不好,我们作为软件工程的学生,要使用专业的 UML 绘图工具。你作为专业的软件工程学生,连专业的画图工具都不会用,怎么行!?”,老师神情严肃地讲道,“我在群里发了两个软件,第一个软件我们把他叫作 EA3,这个软件是商用的,你也可以用第二个开源的 StarUML。4

Visio(以及开源的 draw.io)不被他视作专业的 UML 图绘制工具,并不是说架构师没办法用它们画好图,而是因为它们缺乏与 UML 相关的抽象。这位老师强调,Visio 是矢量图绘制工具,而不是专业的 UML 图绘制工具。这两类工具的区别就在于抽象层次的高低。

之所以这么说,是因为,UML 图明显就是图片的一种,一般也是作为矢量图来绘制的。能绘制矢量图的工具当然也能绘制 UML 图,说白了,UML 图里最常见的元素就是方框、圆圈、直线、箭头、小人和文字,连 PowerPoint 都能画这种图。只要使用了正确的建模语言和规范,保证图片清晰易懂,就是好的 UML 图。

前文提及的 EA 和 StarUML 等软件,当然也是能画方框、圆圈和小人的,只不过它们只能用这些元素来画 UML 图。使用这种工具绘制 UML 图是自然的,哪怕对建模语言本身不熟悉的人,也不容易画出错误的图。

通俗地来讲,这两类软件的区别,就是化学原料和包装好的清洁剂的区别。这位老师觉得:使用一水柠檬酸溶液来清除水垢是不专业的,应该使用包装上写明了「水垢清洁剂」的清洁产品,才显得专业

破除魔法

让我总结一下目前为止我的观点。

长期依赖于高度抽象的图形界面工具,尤其是在一开始就没有了解过低层次抽象的前提下,会让人忽略最基础的知识。这是个人层面的问题。社会层面上,会创造出本不应该存在的门槛和鄙视链,仿佛使用高度抽象的软件系统就更专业。

这种心态不罕见,我相信:如果让所有的 Java 开发者停止使用 IDEA,有相当一部分人会不知道如何启动 Spring Boot 项目(答案:用 mvn spring-boot:run);如果你告诉别人你用 GIMP 编辑图片而不是 PhotoShop,有相当一部分人会认为你不够专业。

这么说来,因为普遍认为前端技术很简单,所以常常被鄙视的前端开发者,实际上大多都脱离了这种窘境,因为前端开发的必要工具之一就是 npm 或者其他相关的命令行工具,我很少看到有人用 JetBrains 的 WebStorm,通过操作图形界面管理依赖项。当然,前端开发也有自己的问题。

使用 LLM 开发也是,许多人宁愿用高度抽象的自然语言描述需求,也不愿意手写代码,就算在遇到 Bug 时跟 Agent 发疯也不愿意停下。5如果他们愿意去看看真实的代码,接触被抽象掩盖的东西,他们就不会如此狼狈。我想这背后也是相似的心态在作祟,而且「程序员应该跟上潮流用 LLM 写代码,手写代码是落后的」这种思想,似乎也有愈演愈烈的苗头。这种潮流,何尝不是痴迷于高度抽象的软件,所造就的门槛?

最后,我想说的是,不要怕麻烦,接触低层次的抽象并不可怕,反而能帮助自己建立起对软件的清晰认知,而清晰是非常宝贵的。就像学习 C 语言有助于程序员理解内存、指针和数据结构等基础概念,就算不用 C 做开发,有了这些理解,也更容易写出高性能、速度快的软件。

从长期来看,对软件的清晰认知,要胜过生产力和专业的外表,而扒开抽象的外壳向内看,是培养这种认知的关键步骤。


  1. 参见: Running commands in projects | uv  ↩︎

  2. IntelliJ IDEA 是 JetBrains 开发的,专注于 Java 和 Kotlin 开发的集成开发环境。一般来说,学校里教 Java,要么会要求学生安装 Eclipse,要么就是 IDEA。IDEA 是最常见的,人们几乎不会思考开发 Java 软件的其他方式。 ↩︎

  3. 全称是 Enterprise Architect,中文意思是「企业架构师」。英文里的 Enterprise 除了指代企业和创业,也和企业家精神和专业性有关联,所以也可以理解为「专业架构师」。 ↩︎

  4. 我了解之后发现,StarUML 也是专卖软件(Proprietary software),最初以 GNU GPL 协议发布了开源版本,但开源版本在 2010 年就停止维护了。当然,就算它是自由软件,我也不想用,原因在于它是 GUI 软件,很重,另一个很关键的的原因是,它是用 Electron 和 Java 写的。 ↩︎

  5. 顺带一提,我甚至见过有 Java 开发者非常认真地讨论,要不要双开 Cursor 和 IDEA 做开发,因为他们觉得需要用 AI 编程,而离开了 IDEA 做 Java 开发又很麻烦。 ↩︎

理想的日常

这是我的「 BlogBlog 同乐会 - 2026 年 3 月 」的投稿文章。本月主题是「 理想的日常 」,由 Alex Hsu 主持。如果你有自己的博客,欢迎一起来参加!


重庆已经到了不需要穿厚外套出门的时候了,总觉得今年的春天来得比以往早。我昨晚三点以后才睡着,今早八点就被阳光叫醒,油腻和不适的感觉裹缠着我的身体。从自己租的房子回到老家之后,生活秩序被打乱,也没有躺在熟悉的床上,总是不能安稳睡去。我记得一个月前收拾行李的时候,恨不得把整个家都搬回去,担心离开之后,住处没有好用的清洁剂、没有好写的笔,担心家里的盆栽会枯萎。

寒暑假我都会找点事情做,以便留在学校,因为生活方式的转变是痛苦的,一周还好,如果是一个月,我甚至会说是不人道的。一两个月的时间不长也不短,不够我建立起新的生活秩序(比如,不值得我为此把老家里所有我看不惯的堆满杂物的角落收拾干净),却又超出了我对生活失序的忍受范围。此外,我极度厌恶被他人审视的生活,我还不需要一个做不到忌嘴的糖尿病患者每天都提醒我不要喝冷水——实际上,除非是锻炼后立刻喝冰水,冷水并不会影响健康,喝太热的水反而会损伤食道黏膜。

说实话,我觉得固执的老人更像大语言模型,他们局限的生活经验就是语料库,未受教育且倔强难改的思维方式就是模型算法,而他们输出的内容,就算毫无道理,语气也带着他们本不该有的自信。要是我以后变成了一个只说不会做,因为自己生活空虚所以只会窥探别人的生活并且指指点点的老头儿,我希望有人能就地处决我,我该死。处决的光荣执行者,将会是为全人类做出贡献,让世界变得更美好的英雄。

好了,牢骚发完了,不依不孝的话也说了。现在,还留在这里的读者们,请允许我为你讲述我理想中的日常。


曾经和现实中认识的人聊天,谈到过「理想的一天是如何度过的?」这个问题,我忘记当时的回答了,唯一还能回忆起的细节是起床的时间。我说,我会在早上八点半起床,当时有人问我为什么不是十点,或者十二点。

我说,早晨很安静。

记不清从何时起,我不再喜欢睡懒觉,折磨我的生物钟并不令人愉悦。换成现在的我,大概会选择早上七点起床,甚至更早。晚起意味着白天的时间被缩短了,而我并不喜欢晚上我熬夜时的样子。据我所知,那个时候的我根本不会按照计划行事,状态也很差1,除非是为了自己有热情的项目赶工,而我们都知道,程序员是不该在晚上把代码提交之后就回去睡大觉的(这句话是 Paul Graham 说的)。考虑自己的健康和项目的安全,在意志力最薄弱的深夜抵抗继续编码的诱惑,就尤为重要——就算大半夜做完了,也不该提交工作,那还不如第二天早点起来做呢。

因为这一两年几乎没有断掉写日记的习惯,有记录自己身体和头脑的感受,也就逐渐在记录中发现,睡眠真的是生活质量的基石。一旦睡不好,我就容易暴饮暴食,运动表现也会明显变差,读书、写作和写代码虽然能够照常继续,但注意力很容易被其他信息夺走。我想,这是因为睡眠影响了认知活动吧。我相信良好的生活应该由七部分组成。

我的良好生活金字塔,记录在我随身的 M5 活页本里

良好的睡眠、良好的饮食、规律的运动、大量和广泛的阅读、创造力、安心的关系和平和的心。这七个组成部分自底向上支撑着彼此,在我看来,要是没睡好,就会忍不住食欲、容易感到疲劳,上面的六项也都会垮台。过往的减肥经历告诉我,对健康的体魄而言,吃比动更重要,皮肤问题和情绪问题甚至也可以通过好好吃饭来解决,只不过现代人很少有能够好好吃饭的。规律的运动,指的是提升心肺能力的有氧运动、提升肌肉力量的抗阻力训练,以及提升柔韧性的静态和动态拉伸——只跑步或者只去健身房撸铁,是臃肿且不平衡的,尤其是不应该忽略拉伸,柔韧性是身体最好的盔甲。

身体之上,才是头脑。这点我和 塞涅卡 的观点完全相反,我和他另一个相悖的观点是,他认为阅读应该忠诚于一个作者,而不是走马观花地什么都读,可我认为阅读应该广泛,尽可能了解不同的人对不同事物的不同看法,尤其是那些与我观点不合的人。不过,在我眼中,高质量的阅读应该是基于长文本的,文章或是书籍,短文本和微博客只起到记录、分享和社交的用途。阅读(还要加上「体验」)是对创作者的滋养,是灵感来源,所以创造力要置于其上。对我而言,写作和创造软件,都是创造力发挥作用的领域。

头脑之上,又是心灵。上面的图片展示的是我随身携带的「良好生活金字塔」,我还在书房的墙上贴了一个更详细的版本,金字塔的每一层都有更详细的划分,不过,「安心的关系」和「平和的心」这两层是空白的,因为我从未体验过这两者中的任何一个,它们是我无法想象的世界。

之所以在开始描写我理想中的日常之前,扯了一大堆没人爱听的理论,是因为我觉得有必要让读者知道我对良好生活的标准是怎样的,目的是建立起共识,以免后文显得突兀。我要是让读者毫无准备地了解到我每天都会把自己的身体掰来掰去,把脚放到头后面去,我认为是会令人感到措手不及的。


回到我的想象。现在是早上七点,日光透过窗户打在我的脸上,不早不晚,正好我把从一个睡眠周期刚结束时叫醒,此时我的头脑很清醒,完全没有起床困难症。我换好衣服,到吧台给自己倒了一杯热水,喝完之后又倒了一杯,很快我的肠道也开始工作,没过多久就进了厕所,并且在两分钟内走了出来,现在身体很轻盈。我洗完手,开始给自己冲咖啡。理想情况下,我是不需要咖啡因来提神的,但至少对现在我的来说,给自己冲咖啡这个动作是我生活的支点。我总觉得,早上要喝的应该是中度或深度烘焙的咖啡,没有太多需要我细细品味、扰乱思绪的风味。

此时我应该能够忍住查看社交媒体和电子邮件的欲望,开始对照前一天晚上列好的待办清单工作。从七点半或八点开始,直到早上十一点左右,我应该就完成今天需要做的所有事情了。据说对于脑力工作者而言,早晨是效率最高的时间段,我自己的感受也的确如此。如果能在头脑最清醒的早晨不受打扰地完成所有工作,剩下的时间就可以留给兴趣爱好、朋友和恋人了(是的,既然是理想的日常,那我应该有一个恋人),并且不会在这些时间里受工作打扰,把生活的秩序搅得一团乱。

十一点半左右,我应该出门赴约中午的饭局。我理想中的内层交际圈,或者说最亲密的那些朋友,他们的数量加上我应该是四人。在四个人的局里,不需要有人一直说话带动气氛,也很少出现其他人话太多导致某一个人插不上话的情况,最安静的人也能在这样的场里待得很舒服,前提是四个人中没有话痨。此外,有很多我喜欢的桌游,玩家数量上限是四人。唔,如果要谈理想的话,这四个人之中,有一个应该是我的男友,不过,我真的很难想象他是什么样子呢。

在某些日子里我可能会和朋友们去桌游店一直玩到晚上,甚至一起在店里点外卖吃饭。桌游比起电子游戏,给我带来的互动感会更强,但也不至于像其他聚会游戏那样过于亲密。不过,在这个理想的一天中,滥用时间是不明智的。吃完午饭后,我就应该和朋友们分开,回到家里给自己冲当天的第二杯,也是最后一杯咖啡。午后的咖啡应该是浅度烘焙的,日晒处理法,有浓烈的水果或花香,在降温后有持续的茶感,可以品味好久。

口腔中还有咖啡的回甘,我开始查看联邦宇宙和 RSS 更新,照顾我的另一个交际圈,看看离我很远的其他地方发生了什么事情。然后是查看电子邮件和即时通讯软件的消息,这个时候我应该收到一些我关注的产品和游戏的新闻报、一些读者的留言、一些求助或询问、联系紧密的网友的消息,这些消息加起来应该不会给我造成太大的负担,不会让我产生推迟到第二天再处理的倦怠感。这些与真实的人的连接应当是令人愉悦的。最后,我会选择一些内容,放到下一期的周刊里。

当我从网络中抽身后,时间应该是下午四点半左右,此时我在电视机前铺好了瑜伽垫,开始当天的锻炼。在这个理想的一天中,我的训练计划应该恰好没有排到力量训练,而是比较轻松的有氧(虽然做 HIIT 也不算轻松,但会让人觉得快乐),结束后略作休息,就开始十五分钟的拉伸,拉伸完毕后是洗澡,此时我的身体应该像刚出生的婴儿一样舒适。梳洗好自己过后,时间是五点半左右,应该开始准备晚餐了。

同样地,在别的日子里,我可能会从零开始买原料,做好几道工艺繁琐的菜,但在这个理想的一天里,冰箱里应该有一些容易处理的食材,或者其他人给我送来的,亦或是我自己提前做好的餐食,稍加处理之后,应该就能在六点左右摆上足够丰盛的晚餐。用餐时,我会和男友一起看《老友记》或者《瑞克与莫蒂》——是的,在理想的日子里,新片子是不被允许的。

晚餐后很适合散步消食,两个人要是能在江边或者海边漫游,应该会很浪漫。我有时会觉得,走出家门散步才是真正的旅行,因为没有行囊,甚至连手机都可以不带,身上没有任何负担,心灵上没有任何束缚,两个人之间也没有任何枷锁,只是在呼吸着、行走着、生活着。漫步也是思考和交谈的绝佳时间,可以消化和整理当天发生的事情,兴许是在餐桌上朋友的小动作,或者是互联网上的见闻。一个人思考可以整合认知,两个人交谈,则可以提供情绪支持,总之是很健康的处理信息的方式。此时路边应该会有跑步的人路过,如果是长相不错的男性,我和男友会有些不道德地一起欣赏,至少在我的想象里,这是作为同性恋者的好处之一。

回到家时大概接近八点了,这个时候我会回到书房里读书,兴许是一本觉得不错,便继续往下读的新书,或者是一本觉得很棒,所以捡起来重读的旧书,又或者,是一本初见便爱上的书,一边读一边想象自己几年后再次打开这本书重读时的感受,这本书的名字或许是《不可承受的生命之轻》。或许,在那个理想的日子里,我会庆幸自己早已体会过了生命中的轻与重、灵与肉,不会陷入托马斯、特蕾莎和萨比娜的苦痛。矛盾的是,在我的想象里,那时的我依旧年轻。

放下书本后,我会开始写作,但愿博客的名字还叫「极客死亡计划」,也但愿我能在上床之前发布文章,因为我不喜欢把草稿晾在 Git 仓库里的感觉。兴许这是一篇前几天写完了一半的草稿,又或许我写得十分顺畅,不需要做太多考究也不需要斟酌,只需要倾泻所思。合上电脑后,我会开始写日记,并依照周计划,写下第二天的待办清单。此时的时间应该是十一点,我钻进了被窝,依偎在爱人的怀抱里睡去。

只可惜,时间好像有些不够用呢。读书与写作这两件事情,对我来说是没办法在一两个小时内完成的。如果我没办法在不压缩睡眠的情况下做完我想做的事情,这又怎么能叫作「完美的一天」呢?我不禁思考,这样理想的日常,真的存在吗?

不过理想就是因为不能存在于现实中,才有美感吧。

是时候回到现实中的生活了。


  1. 事实:凌晨四点时大脑的状态比喝醉了酒还要差。 ↩︎

Jack,你也健身吗?

某种程度,英语里的 Jack 是很传奇的人名,它无处不在。

Jack 是不会说英语的中国人也耳熟能详的英文人名。著名俚语词典 Urban Dictionary 上有 864 页与 Jack 有关的词典(虽然这个网站也以胡说八道的词条居多著称)。令人费解的是,很多工具也以 Jack 命名。如果在公路旁边看到有人举着牌子,牌子上写着「Need a Jack!」,这并不是在说「需要一个男人」,而是说「需要一个千斤顶」;建筑工人用来在地上打孔的冲击锤,叫作 Jackhammer;一种用来脱靴子的工具叫作 Boot jack(反过来,Jackboot 是一种军靴)。

更怪的是,Jack 还和「偷窃」有关系:Hijack(劫持)、Carjack(偷车)和 Skyjack(劫机),Jack 本身作为动词就是「偷」的意思。想必不用我说,一些读者也知道 Jack off 是「手淫」的意思,与 Jerk off 同义。扑克牌里面的 J,也叫 Jack。除此之外,还有 Jack-o-Lantern(南瓜灯)、Jackpot(头等奖)、Lumberjack(伐木工)、Jackrabbit(野兔;兔属;甚至还有一个 Java 内容管理库叫作 Apache Jackrabbit)、Jackass(公驴,也用来骂人)、Jackdaw(寒鸦)、Blackjack(二十一点,一种纸牌游戏)、Applejack(一种苹果酒)。

Jack 的身影遍布这门语言的各个角落,不过今天的文章只会关注其中的一小部分,即健身房里的 Jack。看到 Jack 以各种工具名称出现之后,把它当作动词的用法突然就形象起来了,要么是把什么东西撬起来,要么是把什么东西冲破。带着这个印象,我们来看看以 Jack 命名的各种健身体式。

跳跃吧,杰克!

开合跳的英文单词是 Jumping Jack,在英国叫作 Star Jump(星星跳,还挺形象的)。美国军队管开合跳叫 Side-straddle Hop,直译过来是「侧跨跳」,显然,这个名字更准确,中文里的「开合跳」也从另一个角度准确地描述了这个锻炼动作。如果你从没做过开合跳,可以参考下图。1

正在做开合跳的小人。

所以,明明有更准确的用词,为什么要用 Jack 这个毫不相关的人名呢?英语本身就是 语言界的最大缝合怪 ,再加上 英语的表意效率低下 ,非常依赖言外之意和黑话,有了 Jumping Jack 这类简短的表达,没人会折磨自己的舌头去说 Side-straddle Hop 这类词的。

不过,为什么一定是 Jack?隔壁 John、Dick 和 Tony 不是也广为人知吗?

有一种玩具叫作掷距骨2(Knucklebones),是一种古老的游戏,在许多文明里都能找到类似的变体,简单来说,是一种把小物件(例如骨头、石子)丢起来,然后抓住的反应力游戏。在比较现代的版本里,这种玩具用金属制成,长得很像三维的直角坐标轴。不过,如果不仔细看的话,这些小东西就很像把手脚都张开的小人,就有点像做开合跳时,手脚张开的样子。

这种用来抛的金属小玩具,名字叫作「Jack」。

所以,有没有可能是有人根据这个动作联想到了用来抛掷的这种 Jack,所以才想出了 Jumping Jack 这个名字呢?

很遗憾,不是。根据 维基百科 ,开合跳据说是西点军校的一位军官发明的,他的名字叫作 John Joseph Pershing,他还有一个昵称,叫作「Black Jack」。他作为西点军校指导员的时候,由于非常严格且死板,学员们很不喜欢他。由于他在美国第 10 骑兵团服役过,而这个兵团一开始是作为隔离的黑人单位组建的3,所以学员们给他起外号叫作「Nigger Jack」,后来,这个称呼被弱化成了「Black Jack」4,因为 Nigger 是对黑人的歧视性用语。

所以,因为发明这个运动的军官外号里有 Jack 一词,开合跳也就被称作 Jumping Jack 了?

很遗憾,不是。至少,根据维基百科的说法,Jumping Jack 取自一种玩具的名字(是的,还是玩具)。这种玩具的名字就叫作 Jumping Jack ,在前几个世纪流行于英国、法国和德国等欧洲国家,是一种用木头制成的、有关节的小人。只要拉动小人身体下面的一根线,它的四肢就会动起来,就像是在做开合跳。

Jumping Jack 还有多种变体:Power Jack 要在跳出去的时候做深蹲动作;Squat Jack 则是要在开和合的过程中一直保持半蹲姿态;Half Jack 是幅度更小的开合跳;5Alternating Jack 或者 Scissor Jack 是在保持开合跳手臂动作不变的情况下,左右脚前后交替跳跃。总之,在健身领域,Jack 差不多已经是开合跳的代名词了,各种类型的开合跳都有 Jack 的名字。

亮剑吧,杰克!

Jackknife 是一种核心训练动作,躺在地上,把手臂和腿伸直,往上举,向身体中间靠拢,直到四肢并拢。如果核心力量较弱,身体条件不允许,一些 Jackknife 的变体并不要求手和脚一定要靠拢,只需要核心发力,将四肢支撑住就好。

图源: Jackknife (exercise) - Wikipedia

不同于 Jumping Jack,Jackknife 的词源很明确,这个词的本意是「折叠刀」。如果把人的核心想象成转轴,上半身想象成刀刃,下半身想象成刀柄,把手脚在空中并拢的动作,就像是把折叠刀折起来的动作。

所以,真正的问题是:折叠刀为什么叫 Jackknife?

没人知道。

我翻遍了维基百科、Wiktionary 和 Etymonline,都没有给出确切的词源描述。不过,我倒是误打误撞地在一个讨论刀的论坛6上找到了 相关的讨论 。2013 年,有个叫作 Jack Black7 的网友引用了某个如今打开已经是 404 的网页:

Jackleg is a U.S. southern slang adjective meaning unskilled or unqualified. The term almost certainly comes from jackleg knife (jackknife) and was probably originally used as jackleg carpenter, a carpenter with only the most basic set of tools.

Jackleg 是美国南方俚语,意思是「未受训练的」或「不合格的」。这个词几乎可以肯定源自 Jackleg Knife(Jackknife),有可能最初被用在「只有一把折叠刀的木匠」(jackleg carpenter)这个词上,指代只有最基础的工具的木匠。

所以,Jackknife 最初应该是 Jackleg。 Etymonline 也在 Jack-knife 相关页面中提到了 Jackleg 这个词,里面提到苏格兰方言里有 jockteleg 这个词,也是一种折叠刀,但词源不明。

折叠刀不仅衍生出了一种锻炼的名字,还衍生出了别的含义。作为动词,Jackknife 表示把东西从中间折叠起来(我真服了,你们就不能老老实实用 Fold 这个单词吗?)。Jackknife 还是一种跳水动作,和核心训练动作很像,也要把手臂靠近腿部,将身体折叠起来。

Jackknife 还是一种口语表达:如果一辆卡车发生意外,车头朝向一个方向,而车厢朝向另一个方向,两者形成的角度过小,就像是折叠起来了,这种事故就被称作 Jackknife,这个意思也可以做动词。

Before I knew what was happening, I’d jack-knifed the truck.

我还没意识到发生了什么,我已经把卡车撞成折叠刀了。

不过,我还是不知道折叠刀和 Jack 到底有什么关系!

波巴和奇奇,杰克向你问好!

有一类难以解释的语言现象叫作 Bouba/Kiki 效应 。观察下面两个图形,凭借直觉选择,你觉得它们哪个叫 Bouba,哪个叫 Kiki?

我觉得他们都叫 Claude

全世界的人类都有着相同的倾向,大部分人都会觉得那个圆圆的图形叫 Bouba,而尖尖的图形是 Kiki,人们会把声音和图形联系起来。神奇的是,最近 新发表的一篇研究 发现,雏鸡也存在 Bouba/Kiki 效应,小鸡崽在听到 Bouba 的声音是会自发选择圆润的图形,听到 Kiki 时会选择带尖角的图形。

我在猜测,人们之所以把没有实义的人名 Jack 用在这么多地方,也可能是因为 Bouba/Kiki 效应。至少对我而言,Jack 这个词的发音很容易联想到插、打、锤、砸等动作,比如把长条状的工具插进汽车车窗里,借此把锁撬开偷车的动作(联系到「偷窃」的意思),或者把铁条插进脚和靴子的缝隙里,借此拉开距离,方便把脚从靴子里拿出来(联系到 Boot jack 这个词)。

我在做卷腹的时候,也能感觉到自己不是在轻轻地卷,而是在「Crunch it!」,Crunch 这个单词的发音也带有一些微妙的力量感。这种感觉很难描述清楚,就像很难用语言和逻辑解释为什么圆圆的图形叫 Bouba 而不应该叫 Kiki 一样。

不过,Jackrabbit、Jackass 和 Jackdaw 的用法,应该是把 Jack 这十分常见的男子名当作「雄性」的代名词了,用来表示公的动物,与上述语言现象无关。


就到这里吧,在电脑面前翻各种网页,坐了一两个小时,还吃了夜宵,现在要站起来做几个开合跳才行。


  1. 真的很难忍住不在这里放一张「Happy~ Happy~ Happy~」的 GIF 图。 ↩︎

  2. 我印象很深刻,因为《咩咩启示录》这部我很喜欢的肉鸽游戏里就有「掷距骨」小游戏,虽然和现实中的掷距骨有很大不同。现实中的掷距骨参见: Knucklebones - Wikipedia  ↩︎

  3. 参见: 10th Cavalry Regiment (United States) - Wikipedia  ↩︎

  4. 参见: John J. Pershing  ↩︎

  5. 参见: Jumping Jacks - Wikipedia  ↩︎

  6. 叫作 Blade Forums ,很神奇,互联网上居然存在这么小众的论坛。 ↩︎

  7. 这名字…… 你该不会是黑杰克军官转世吧? ↩︎

❌