对编程语言“孰优孰劣”的一些理解

对编程语言“孰优孰劣”的一些理解

编程语言标准与编程语言实现

我很想提出的一件事是所有高级编程语言本身在实现上是没有上下之分的。很多人听闻 Python 的官方解释器 CPython 是用 C 写的,就以为 Python 只不过是基于 C 的一层封装,认为关键还是 C,学好 C 就学会了一切——这是极其错误的。

编程语言本身指代的不是任何一种实现,而是一种规范。一门语言的编译器和解释器如何实现,实际上只是细枝末节的实现问题,和语言自身的标准无关。Python 的解释器可以拿 C 写,可以拿 Java 写(Jython),可以拿 C#写(IronPython),可以拿 Rust 写(RustPython),甚至可以拿 Python 写(PyPy)。要是不在乎性能问题,也没人说不可以拿 Ruby 甚至 PHP 写,吃饱了撑的拿 FORTRAN 都能写。

JavaScript 那边有极多玩具解释器,其中不乏用 Python、PHP 甚至 Lua、Kotlin 等很多人完全想象不到可以用来写编译器/解释器的语言写的。这些解释器,效率肯定不如 V8,但都是可以实现的。

现在我们看到许多主流语言将解释器/编译器基于 C/C++/Rust 实现,或者至少它们非自举版本的解释器/编译器基于这些我们通常认为“偏底层”的语言实现,那只是为了性能考虑——但说实话,如果做好 JIT,不用这些偏底层语言性能一样好,比如 PyPy。

大多数主流语言的官方编译器/解释器其实都是自举的。Go 的编译器是 Go 写的,C#的编译器是 C#写的,Java 的编译器(前端)是 Java 写的,而 C/C++那块自然是 C++写的。

甚至连 Racket——很多人应该没听过这个名字,它是 Scheme 的现代继承者——最近都把解释器改成了“自举”的,加个引号是因为现在 Racket 的解释器是 Chez Scheme 写的,而 Chez Scheme 其实是基于 C 实现的,而 Chez Scheme 的语法本身就是 Racket 的一个子集。Racket 社区选择这么套娃主要是为了证明咱们 Lisp 也可以自举 Lisp,至于带来的性能损失(指性能还不如 Python 的一半)——Lisp 社区一直不大在意。如果在意的话,可以去用 Clojure 啊!虽然 Clojure 已经被开除 Lisp 籍了。

所以关键问题是,语言本身的实现是很次要的一件事。虽然在实用上这再重要不过,但我们要把语言标准和语言实现分开来看。说实话,我一直以来看到什么类似“Python 更新 3.11,提速 30%”或“Ruby 更新 3.0,提速三倍”之类的话都觉得多少带点别扭——这里将语言等同于它的官方实现了,但这俩语言都不只一个实现。当然,我们在 C/C++这块就很少听到类似的说法,毕竟 C++本来就是 MSVC、GCC、Clang 三家分立,不分上下,所以不经常看见有人直接拿 C/C++直接指代这三者其一的。

底层并不意味着“高级”

有时候我怀疑很多人不会区分编程语言的标准与实现,其根本原因是许多人会在脑中自发地形成一条编程语言鄙视链,觉得越偏底层越厉害,对于大多数人而言,那就是 C/C++了,那自然会以这个方向去思考问题。

有时候看到有人吹嘘 C++多么厉害,什么软件都是拿 C++写的——Office、Chrome、操作系统(主要是 UI,非底层)、Unity 之前的大多数游戏、MySQL、大量的基础设施……但这是因为那时候只有 C++能用而已——总不能用 Java 吧。现在微软那边一直在用 C#替换内核代码,再过几年应该能看到 C#大量侵入 Office 了;Linux 已经在大量引入 Rust 了,尽管这可能带来 5%~10%的理论性能损失,但相比于弱类型的 C 带来的 BUG 来说,这根本不是问题,说不定少了一大堆 BUG 性能反而能大幅提升了;游戏开发则早已多数迁移到使用 C#开发的 Unity;至于数据库,实际上很多数据库都在使用 Rust 替换底层,有些会使用 Go 做上层,比如 TiDB;基础设施这边,新出来的基础设施已经很少直接用 C++写了,有用 Rust 的,也有用 Go 的。

我们其实应该认清楚 C++的统治地位已经不如以往了,虽然它在音视频解码、图像处理、工业软件开发等领域的地位暂时还不可替代,但它终究是太老了。与其给它续命,不如让它慢慢结束自己的超长服役期。

——不过暂时看来,C++有可能以另外一种形式继续苟下去。C++标准委员会现在看起来就是想在这上面发明一门新语言,连int main都进化成auto main() -> int了,而std::vector<int> nums = {1, 2, 3}也进化成auto nums{std::vector{1, 2, 3}}了,模块也加进来了。未来说不定 C++要引入个和 ES5 类似的严格模式,禁止旧语法的使用,到时候看起来就和 C 是完全两门语言了。我很期待全是 auto 的日子的到来。

低抽象与高抽象——两种方向

不谈 C++,谈谈语言本身。其实编程语言有两种方向,即低抽象与高抽象。低级抽象,如 C,直接将内存暴露在程序员眼中,随便操纵地址。别看大家都说 C 恶心,弱类型、随便操纵指针引发成吨 BUG,而 Rust、Go 把指针运算直接放到 Unsafe 里了,但有些时候很多偏底层的逻辑 C 操作起来就是方便,虽然 Rust 这类也能做,但确实没有 C 方便。这类语言的逻辑就是直接对应到机器具体执行上去,熟练的 C 程序员是很容易直接从 C 看到汇编上去的。而 C++就不同了,语法太多,而编译器又做了太多魔法优化,根本没人搞得懂会编译成啥样。

稍微往后一点的语言,引入面向对象的思路进行建模,如 C++、Java。但它们在业务逻辑上实际上还是 C 的思路,一堆循环、flag 变量、if 语句。在编写业务逻辑上,它们的思路还是让程序员“写出代码的具体执行步骤”,而不是“告诉程序要怎么做”。其实这就是SELECT * from tablefor (int i = 0; i < table.rows.length; i++) { ... }的区别,而 C++和 Java(至少在当时)显然是后者。

后来的语言多少受到了一些函数式编程的影响,抽象程度进一步增强。如 JS 那边 ES5 引入的 map/filter/reduce,Python 从 Haskell 里抄来的推导式。当然 Java、C++这些语言也与时俱进,Java 8 引入了 Stream API,虽然写着还是吃力,但也算是有 map/filter/reduce 了;C++与众不同,就像 unordered_map 这个诡异名字一样,它对于 map/filter/reduce 也有一套独特的命名规范,那就是 C++ 11 加入的 transform/remove_if/accumulate(其实 17 有 reduce 来着)。当然,我们可喜可贺地看到许多现代编程语言都有了这三个基本的组合子,这代表了编程语言抽象程度的进一步提升。

很多时候我们说什么类似“你代码写的不 Pythonic”这种话,其实说的是代码抽象程度太低。比如 Python 里有列表推导式不用,偏偏写个双层循环放一堆 if,这就是不 Pythonic。当然要是有人写 JS 从不用 map/filter/reduce/flatMap/some/every,总是要定义个布尔类型的 flag,搞一堆循环,弄几个 if,再加几个 continue 和 break,除非你的代码要兼容 IE6,否则也肯定有人要看着不爽。比较悲伤的是 C++和 Java 那边不这样,就算你不用组合子也没人会觉得有什么不对。2014 年 Java 出的 Stream API,现在还有那么多人从不用 lambda!

这几个组合子当然只是函数式的皮毛。再往上一些有比如 Maybe、IO Monad、Either 这些Functor对可能不返回值的函数、有副作用的外部操作(如 IO)、错误处理等做抽象。在这个程度上,抽象是为了将一些核心逻辑从语言内部剥离,放到库函数的实现上,比如这里很大程度上就是要把异常给剥离出去

然后,很多“纯净”的语言是不提供循环的,只有递归,并且认为其实你通常情况下也用不到递归,map/filter/reduce(或者叫 foldLeft/foldRight)可以解决绝大多数问题。考虑到性能问题,一般这些语言会做尾递归优化。其实这么做的原因很简单,因为循环本身的求值顺序是很怪异的,while 括号里的东西其实可以被理解为一个函数闭包,每次循环都要做一次判断,是没法直接求值的,这很奇怪,所以去掉是理所当然的,反正大家可以用递归和 Y 组合子,没什么影响。

除此之外,有些语言有趣地将语言中大多数东西都实现为了。当然这个宏不是 C 的那个——我得说 C 把它那个东西叫“宏”严重影响了大众对 Macro 这一概念的正确认知,C 那个应该叫字符串替换,请大家不要误解了。这样做有个很有趣的好处,就是像 if 语句这种也可以在管道里传递了,比如fn |> if(condition)这样的写法都是合理的了。这进一步增强了语法的简洁性与通用性,减少了很多特殊情况。

函数式语言往上便是逻辑式语言,如 Prolog。比较遗憾的是由于抽象程度太高,很多时候逻辑式语言解决问题反而会带来更大的思维负担,所以很少有人用。不过逻辑式语言的好处是做查询很方便,比如有些图数据库将查询语言做成像 Prolog 的三元组格式,就很合适。逻辑式语言也很方便解决一些数学证明问题,比如八皇后问题,只需要告诉 Prolog 几条基本规则就可以自动算出来,不需要你编写具体的计算逻辑。比较遗憾的是,这种偏向数学的应用场景使逻辑式编程要真的被广泛使用困难重重。目前我见到被最广泛使用的近似逻辑式编程的范式在 Wolfram Language 里,也只有数学工作者会用到。

总体上讲,我们不应该真的对编程语言做高下之分。在我看来它们只是对代码逻辑不同程度上的抽象而已。图灵完备的语言能做的工作都是一样多的,没有孰优孰劣。


对编程语言“孰优孰劣”的一些理解
https://snowfly-t.github.io/2023/02/03/对编程语言“孰优孰劣”的一些理解/
作者
Snowflyt
发布于
2023年2月3日
许可协议