本文最初发表于 David 的网站,经授权在此转载
去年一月,Exercism 宣布了一个名为12in23的新项目,邀请参与者在 2023 年尝试 12 门新的编程语言。每个月都有一个主题(比如“Analytical April”或“Object Oriented October”),并会重点推荐一些具体的语言供你尝试。我热爱学习新事物,也已经算得上半个(编程)语言极客了,于是决定试一试。12 门语言,12 个月!
现在这一年即将结束,我对这个项目的进展非常满意。我成功尝试了 12 门新语言,在 Exercism 社区里结识了很棒的人,还顺路提交了一些不错的开源贡献!在这篇文章里,我会逐一回顾这些语言,并聊聊我从每一门语言中收获了什么。
挑选语言
为了充分用好这一年的体验,我给自己定了几条原则:
- 语言要么对我来说是全新的,要么至少足够陌生,让我觉得自己能学到很多东西。
- 选中的语言应当(有潜力)对我在未来深入学习有实际用处。这个项目本身只是为了好玩,但我希望花出去的时间学到的东西(至少部分)是有用的。
- 不管用哪门语言,我都会装好本地工具链和 VSCode 插件。我想在尽量平等的条件下比较这些语言,尽可能有完整的类型提示和智能补全。上大学时,我所有的编程作业都是在 Sublime Text 里写的,没有任何代码检查或自动补全。我当时担心,如果一边用这些工具一边学编程,会过于依赖它们,成不了一个好程序员。结果恰恰相反。我能交给工具处理的认知越多,就越能把精力放在眼前实际的问题上。不要记住东西,要记住怎么找到它们。
开始吧!

January (Unthemed)
一月开始时,Exercism 团队还在敲定每月的主题,所以那个月选什么语言由自己决定。没什么方向,我就用 Go 给这一年开了个头。2022 年年中我上过一门 Go 的速成课,但之后没怎么用过,完全谈不上熟练。
Go 是一门有意思的语言。它严格的编译器意味着“你的程序一定是对的”,在它认为你可以安全通过之前,你一步也别想往前走。1 它那种啰嗦的错误处理方式意味着你永远不会遇到意外(代价是把if err != nil { return err }写上 太多太多 遍)。它很擅长把难的事情变简单(比如通过通道实现的并行),但也会把一些简单的事情变难(字符串操作)。它有健壮的标准库,意味着不用第三方模块就能完成大多数任务。我很喜欢这个生态里很多东西(格式化、安装、构建等等)都是官方的,直接内置在go命令里。这门语言有不少批评者,但我认为它在“正确性”和“可维护性”这两个目标上基本是成功的。
我还没喜欢到会把它当作第一选择,但对于对性能敏感的场合,它是我工具带里的一件利器,比如在 shell 提示符里显示嵌套路径。
Functional February

二月直接一头扎进深水区,聚焦函数式语言,它们是更常见的命令式编程语言的一个偏数学的分支。函数式语言以“纯”函数(没有副作用)著称。我选了 Elixir,主要是因为我的朋友 Caleb 用它来刷 Advent of Code,而且赞不绝口。
我用 Elixir 的那段时间相当愉快。它受 Ruby 启发(这很合理;它的创造者 José Valim 曾是 Rails 的核心贡献者)。我发现用它表达函数式概念,比如方法链,非常简单。我很喜欢那些让这件事变得容易的语法糖,比如管道操作符(|>):
foo(bar(baz(new_function(other_function()))))
# becomes
other_function() |> new_function() |> baz() |> bar() |> foo()
这也是我第一次接触宏,也就是“写代码的代码”。由于 Elixir 程序可以表示成一棵本身也是合法 Elixir 代码的 AST,所以写代码来生成其他合法代码很容易。这是个很酷的概念,Elixir 把它变得简单了。我还很喜欢函数可以按参数形状进行模式匹配,这样函数调用就能被分发到合适的实现:
defmodule TuplePrinter do
def print({a}) do
IO.puts("single")
IO.puts(a)
end
def print({a, b}) do
IO.puts("double")
IO.puts(a)
IO.puts(b)
end
end
TuplePrinter.print({1})
TuplePrinter.print({2, 2})
# single
# 1
# double
# 2
# 2
这类特性感觉要么很棒,要么会把你的代码写得一团乱麻。不管怎样,这个概念都很酷!
Elixir 还得益于运行在 Erlang 的 BEAM 虚拟机之上,这让它能对接一个庞大的生态。它非常擅长并发,也是广受欢迎的 Phoenix Web 框架的核心。
虽然我眼下没有用 Elixir 的需求,但用它写代码真的很有趣,绝对是我愿意再回头用的语言。而且,用不熟悉的方式(也就是递归)去解决熟悉的问题,本身就是个有意思的挑战。
Mechanical March
三月聚焦“系统”语言,也就是会编译成机器码的语言。
在可选的语言里,我唯一感兴趣的就是 Go。2 眼尖的读者会注意到,我已经用 Go 学了一个月,所以再重复一遍不能算进我的 12 门里。可当初一月选它的时候,官方还没宣布会做主题,所以我没意识到自己把自己逼进了死角。
如果我早知道会有 Bun,我大概会去试 Zig,但很遗憾,我(还)看不到未来。所以,既然没有更有吸引力的选择,我就多学了一个月 Go,心里清楚今年晚些时候还得再补一门才凑得齐。
Analytical April
四月的主题是数据科学领域流行的语言。我对 Python 太熟了,而 R 我在大学的统计课上用过(不太喜欢),所以就是 Julia 了!
我用得还挺开心,但主要是因为它感觉太像 Python 了。这有点让人不安,就像美国人到了加拿大:一切都非常熟悉,却又在难以言说的地方差了 一点点。突然有人递给你一枚两元硬币(或者一个简直 天生 适合做矩阵运算的函数),你才意识到自己已经不在堪萨斯了。
最让我印象深刻的是 Julia 的类型系统。它像 Python 的类型系统那样(可选地)标注类型,但会在运行时检查,确保实参与声明的类型一致。我认为 Python 的类型系统在“能接入工具”和“不挡你的路”之间找到了恰当的平衡,但我也承认,Julia 针对类型不匹配的函数给出的运行时错误同样有用。
总的来说,Julia 很不错,但我觉得以后大概用不到它。
Mindshifting May
五月把“试试新东西”推得更远,重点介绍那些做法非常不寻常的语言。我借这个机会尝试了人气一直很高的 Rust。不得不说,我明白它为什么这么火了。
虽然“臭名昭著”的借用检查器确实需要时间适应,但我喜欢它逼我更仔细地思考自己的程序。编译器确实严格,但它的错误信息在帮我解决问题上做得格外到位。我不会说自己第一周效率有多高,但至少能看见学习曲线的高处了。
Rust 的包管理器cargo也值得特别一提。虽然我没有装任何第三方包,但它的构建、测试和格式化功能都很棒。它的 VSCode 插件也一样,我在像 Rust 这样静态类型的语言里期待的各种便利它都有。好的开发体验真的会带来天壤之别。
虽然在实现层面两者差别很大,但就我想用它们做的事情(让程序跑得非常快)而言,Rust 和 Go 给我的感觉很像。我平时常用的那些语言里,许多工具正因为性能上的特点开始转向 Rust,所以我预计以后会越来越常见到它(即使我自己不写 Rust)。
Summer of Sexps (June)
六月是 S-表达式的月份,这是 lisp 里常见的一种语法形式。我选了 Clojure,一门运行在 JVM 上的函数式语言。
很多年前我写过一点点 Clojure。那时我刚毕业,在第一份工作里成了某个业务关键的每日脚本的唯一维护者。不用说,那段时间并不轻松。我很好奇,如今自己年长也更有经验了,它会不会变得更好上手一些。
很高兴地告诉大家:确实如此!二月积累的函数式经验帮我学会递归地思考,而它的语法一旦上手,其实也没那么糟。如果把它用在更大的项目里,它与 JVM 的互操作性也会很有用。
在有其他选择的情况下,我想我不会再用 Clojure 做什么了,但这段经历总体来说并不难受。
Side Quest: The Universal Test Runner!
多年来,我一直用一个小的 bash 函数来运行当前目录下的单元测试。在折腾这些新语言的过程中,为了图方便,我不自觉地往里加了不少行;记住运行t比一遍遍重新回忆各语言的测试命令要容易得多。
当需要的逻辑超出了我对 bash 的舒适区,我在六月花了些时间把这件事独立出来,做成了一个项目:通用测试运行器。
我在 Exercism 论坛上分享了它,反响不错。大家很喜欢,于是我们决定把类似的功能做进 Exercism CLI 本身(它是用 Go 写的,而我恰好不久前刚复习过这门语言)。于是在下半年的每个月,我都能用exercism test来运行那个月语言的测试套件(这条命令在通用测试运行器里是原生支持的)。
如果你想进一步了解这个过程,我在它发布时写得更详细。
好了,继续!
Jurassic July
七月展示的是老语言。就实用性而言,这个月可选的确实不多。我先从老牌的 COBOL 开始,因为我听说很多关键基础设施还在用它跑。但八月初的一场婚礼正步步逼近,我没有精力坐下来学一门对我来说如此陌生的语言。于是我换成 Visual Basic,因为它是看起来最不差的那个选择。
这里没什么好多说的。这门语言看起来有点啰嗦,但用起来还算容易。据我所知,它其实是为 Windows 上的 UI 开发设计的,所以做点小练习很难对它形成完整的判断。
Appy August
八月满满都是用来构建应用的语言。不出所料,这个月的选择很多。我选了 Swift。作为一个用很多 Apple 产品的人,他们自家打造的语言跟我很相关。我并不是完全没接触过它:2016 年我用纯 Swift 发布过一个 iOS 应用。但之后我再没碰过这门语言,而它已经演进了很多,所以我觉得它还是算数的。
它用起来这么顺手,让我很惊喜。和这里其他许多语言相比,Swift 相当新。它最早在 2014 年发布,显然从现代语言设计的经验中获益不少。它有官方包管理器、可选链、一等函数,以及合理的字符串插值。即使不用 Xcode,读起来写起来也很顺手。
话虽如此,Swift 主要还是在 Apple 平台应用的场景里有用,而我现在不写这类应用。虽然它做练习很好用,但我短期内不打算再回来用它。不过,我确实很喜欢能在 iPad 上写它!
Slimline September
九月探索的是非常简洁或小巧的语言。我选了 jq,一个我用了多年、也很喜欢的工具。
我一直只把它当作处理 JSON 的工具,而不是一门通用编程语言。结果惊喜地发现它有一切常规配置,函数、变量、循环等等,所以我还能写出相当复杂的程序:
# input: { "series": "1", "sliceLength": 1 }
. as {series: $series, sliceLength: $sliceLength} |
if
$series == "" then
"series cannot be empty" | halt_error
elif $sliceLength > ($series | length) then
"slice length cannot be greater than series length" | halt_error
elif $sliceLength == 0 then
"slice length cannot be zero" | halt_error
elif $sliceLength < 0 then
"slice length cannot be negative" | halt_error
else
.
end
| [range(0; $series | length)]
| map($series[. : . + $sliceLength])
| map(select(. | length == $sliceLength))
试用那些我在简单数据转换里从没用过的 jq 功能很有趣。虽然这边的工具链稍显不足(没有编辑器集成等等),但更深入地了解 jq 功能的广度还是很有价值。
补充:Mastodon 上的 DJ Adams 提醒我留意到 jq-lsp 项目以及它对应的 VSCode 插件。这次我错过了,但以后会去看看。
Object Oriented October
十月深入面向对象语言。我对面向对象的设计情有独钟,它很接近我脑中构思程序的方式。我选了 Ruby,这听起来可能有点奇怪。
我在 Stripe 工作,这里是世界上最大的 Ruby 代码库所在地。那它应该不算“陌生”语言吧?话是没错,但我们那个 Ruby 单体应用和“标准”Ruby 相差甚远:一切都用 Sorbet 做类型检查,有 大量 代码生成,我们还做了很多“魔法”让所有东西协同工作并支撑起规模。虽然 Stripe 内外的 Ruby 终究是同一门语言,但在如此不同的规模下工作,体验天差地别;我想知道“外面”(也就是我大量使用 Ruby 之后这些年)的生活是什么样。
总的来说,感觉不错!Ruby 本身很棒,它把“程序员的幸福”列为主要目标,这点很打动我。我喜欢常常能猜出自己从没用过的标准库函数的名字。我喜欢构建函数式代码如此容易,也喜欢它的语法如此顺手、富有表现力。
话虽如此,开发工具比 Python 落后这么多让我很意外。也许是我被惯坏了,但编辑器里的类型提示,加上极快的 lint 和格式化,对我来说比我自己意识到的更重要。对一门在巅峰期像 Ruby 这样流行的语言来说,它在这方面感觉落后那么多,让我很吃惊。3 我也始终没太适应函数调用里可选的括号,这让把函数当作实参传递变得不那么直接。
Ruby 仍然是一门很棒的语言,我在工作中也会继续用它,但它能做的事 Python 都能做,至少目前是这样。
Nibbly November
十一月是迄今最难的一个月:汇编语言。虽然现在很少有人手写汇编了,但熟悉这个话题既有用也有趣。我选了 WebAssembly,因为它对当下以及未来的 Web 都很重要。虽然它通常被当作编译目标(不是给人手写的),但世上确实存在面向那些疯子的工具。
这个月我准备得出乎意料地充分。语法感觉像 Clojure,语言结构感觉像 Zachtronic 的 TIS-100。奇怪的是,我还挺享受每个操作都要从零开始写的感觉;有种古旧的味道。如果真要用这种方式把事做完,我会受不了,但当作一时的小玩意儿还挺有趣。加上充足的注释,我写出来的东西几乎能读得懂了:
(module
(func (export "eggCount") (param $number i32) (result i32)
(local $res i32) ;; result
(local $remainder i32) ;; loop counter
(loop $loop
;; $res =
(local.set $res
;; $res +
(i32.add
(local.get $res)
;; $number % 2
(i32.rem_u
(local.get $number)
(i32.const 2)
)
)
)
;; $number //= 2
;; (keep on stack)
(local.tee $number
(i32.div_u
(local.get $number)
(i32.const 2)
)
)
;; this will keep looping until remainder is 0
br_if $loop
)
local.get $res
)
)
最大的障碍是缺少文档和资源。甚至连有哪些全局函数都很难搞清楚。不过既然我实际上不会用它,一旦上手跑起来,也就没那么在意了。
十二月用那些不适合归入其他类别的语言为这一年收尾。由于三月重复了一次,这个月我需要完成两门语言。
我从 Wren 开始。它由 Bob Nystrom 创造,他除了其他成就之外,还因Crafting Interpreters一书而闻名。它注重细节、体积小巧、采用自顶向下的设计,这些都让我着迷;一切似乎都经过深思熟虑。这种用心在变量作用域和私有性规则的细节里都能体现出来。它的编译器很小,而且注释非常丰富,所以如果你对语言实现感兴趣,它是很好的学习资料。
Wren 有点粗糙,而且看起来基本被弃置了,但对一门玩具语言来说我觉得没关系。没人会指望它能上生产。这个世界当然容得下非生产用途的语言。
另外:Lua
这个月我选的第二门语言是 Lua。和 Wren 相反,它极其实用。它容易嵌入,所以会出现在很多地方,比如 Redis 脚本和 Factorio 模组。它的对象模型需要适应一下,但我能看出自己很快就能上手。我很快就喜欢上了用表这种万能的表结构。工具链也不错:包管理器开箱即用,VSCode 插件也能毫无麻烦地支持基于注释的类型标注。
虽然我目前没有立即需要 Lua 的地方,但由于它应用广泛,它是工具箱里又一件好用的工具。
收尾

这次语言之旅比我预想的更有趣。我不仅学到了一些实用的新技能,还感觉自己的视野被彻底拓宽了。
至于接下来做什么,我想是更深入地学 Rust。它在开发者工具领域的地位如今已经很明显,我想确保自己能读懂、也能为自己依赖的东西做贡献。
我并没有具体要交付什么的想法,但我有整本 Rust 书要读,有一门面向 JS 开发者的 Rust 课程(我之前报销过),还有一整个 Exercism 学习路线要完成。我想至少给一个开源项目做点贡献(很可能是 Just,我最近很喜欢的一个程序),不过就看这一年会把我带向哪里了。
在那之前,祝你节日快乐,2023 年余下的日子也过得开心!