xavdid 上传的头像

完成 #12in23 之后的一点感悟

@xavdid
2年多前

本文最初发表于 David 的网站,经授权在此转载

去年一月,Exercism 宣布了一个名为12in23的新项目,邀请参与者在 2023 年尝试 12 门新的编程语言。每个月都有一个主题(比如“Analytical April”或“Object Oriented October”),并会重点推荐一些具体的语言供你尝试。我热爱学习新事物,也已经算得上半个(编程)语言极客了,于是决定试一试。12 门语言,12 个月!

现在这一年即将结束,我对这个项目的进展非常满意。我成功尝试了 12 门新语言,在 Exercism 社区里结识了很棒的人,还顺路提交了一些不错的开源贡献!在这篇文章里,我会逐一回顾这些语言,并聊聊我从每一门语言中收获了什么。

挑选语言

为了充分用好这一年的体验,我给自己定了几条原则:

  1. 语言要么对我来说是全新的,要么至少足够陌生,让我觉得自己能学到很多东西。
  2. 选中的语言应当(有潜力)对我在未来深入学习有实际用处。这个项目本身只是为了好玩,但我希望花出去的时间学到的东西(至少部分)是有用的。
  3. 不管用哪门语言,我都会装好本地工具链和 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 年余下的日子也过得开心!

  1. 未使用的变量也算编译错误??拜托 ↩

  2. 其实我先试的是 C++(大学之后就再没写过)。它实在不好玩,所以我放弃了。 ↩

  3. 这是“真正的”Ruby 与我在 Stripe 的体验不同的又一处,所以我很高兴两种方式都试过。 ↩

2024年01月05日 · 觉得有用吗?