通用语言模型


Author: Kimmy

如果使用过超过一种编程语言(我觉得大部分人应该都有这种经验),那多少也都知道,编程语言在某些方面存在一些共性。这是很正常的,毕竟作为演化了几十年已经成为标准工具的编程语言,都是源于几个基础的模型。基于共同的模型找共性,那自然不是问题。

问题出在想要用这种共性做些手脚上。比如能不能找一个统一的模型,可以把大部分(先不说所有)的编程语言都统一起来,有一个公共的AST和IR,然后通过这个公共AST可以转换为不同语言的表示,并且还能做到同步。

这不是什么异想天开,乍一想之下还是感觉能够行得通。我在2014/2015年的时候也想到过类似东西,并且实践了下。毕竟,微软还有Common Language Runtime,JVM上还能跑那么多种不同范式的编程语言,LLVM就是个为了不同语言打造的公共后端,甚至不说这种底层运行时,JavaScript在早几年还是各种AltJS的target。

但这只是因为乍一看是能行得通的。各种AltJS之间简直就是生殖隔离,唯一的共性就是都能编译到JS;F#为了能够成功编译到CLR,一些典型的构造(比如,模式匹配)都得换成奇怪的手法实现;Java和Scala的互调用也全是血泪屎,所以知名的Scala并发库Akka不得不提供针对Java的API。

因为很简单,前面提到的所有的“公共”或者“通用”的部分,都是“运行时”。LLVM虽然是IR,但也到了一个非常贴近运行时模型的结构了,这些结构本身是很难还原其上层的表达结构。简单举个例子,同样是循环,我可以使用for、也可以使用while,而最终所编译出来的结果可能是几个一摸一样的条件跳转。

对于任何一个编程语言,我们假设他的代码结构是A、其AST的模型是B、对应IR的模型是C,最终运行时呈现的结构是D。A、B、C、D这样一个单向的流程,就是编译、链接、加载和运行的过程,但如果要逆向这个过程,就需要携带足够多的信息(比如Java class文件中的符号表,比如msvc的pdb文件)。

造成target到同一平台的不同编程语言生殖隔离的问题就在于此。绝大多数运行时平台所采用的结构,都是其主要对应的编程语言语法的线性序列化。比如JVM的Bytecode对应到Java的代码、比如汇编指令对应到C的代码。而任何在这之上添加的更加复杂的模型,就需要绕过像C/Java本身,直接对自身模型的逻辑进行单独的构造。比如早期的Cfront(第一个C++编译器)就是编译到C语言,但后面还是选择放弃了。Scala/Kotlin虽然也直接编译成了Java类,但其生成的代码有些甚至是Java都没办法表达的。

我们跳出单个的编程语言或者平台,来做一下横向的对比。

首先对编程语言做一下简单的分类。考虑两种分类方式:第一,语言的动态性,既编程语言在编译时或者运行时动态操纵代码逻辑的能力;第二,语言的复杂度,即编程语言特性的丰富程度。比如Ruby、Smalltalk这种随时可以在运行时动态修改执行环境的语言,其动态性就更强;再比如D语言这种可以通过模板来编译时生成代码、有UFCS、CTFE等编译时特性,其复杂度就更高。

由此我们可以把编程语言分成下面四个象限(各象限里面的排序比较主观,请不要过度纠结,我们还是更多看象限的划分)。

这样划分以后,能够有非常明显的区分:

由此可以举出来数不胜数的例子。C++的template parameter pack目前只有D中有类似的结构,对应到了C#,可能一个类模板要变成几十几百个泛型类;这个时候如果C#中更改了其中一个泛型类的实现,C++侧就炸了,只能按照C#的模型生成几十几百个特化。Ruby的method_missing在静态语言中也只有D有opDispatch对应支持,换成其编程语言只能干看着了。

如果我们把这种上对于下和右对于左的关系用高阶和低阶来指代(仅仅表达其对应关系,并不是说确实有高低贵贱之分),那高阶语言所能够表达的结构,在低阶语言里对应就变成了难以正常理解和维护的存在,那这样的对应关系即使使用通用模型可以表达,也没办法作为交流的媒介。

再次强调一下这里的高阶和低阶,只适用于本文中是否可以由低向高转换的概念,并不是在说语言的高低。毕竟很早之前我就有了一个自诩为“几米第一定律”的结论,任何编程语言都是领域特定的。

我们并不否认,甚至还要感激通用语言运行时的抽象给大家带来的好处。但不同的编程语言是用来表达的工具,运行时只是他们最终呈现的效果。如同不同的媒介和文体一样,必然有着其本质的区别和适用的领域。可以选择用他们来表达同一件事情,但非要有一个统一的模型来去组合和转换,就有点过于强行了。

创建时间:2021-01-30 最近更新时间:2023-11-03