编程语言胡言乱语

在编程领域我只是一个 beginner,读书不够多,此处是一点不成熟的拙见,甚至漏洞百出,请读者阅读时抱有质疑,同时欢迎批评指正。

在接触了各种编程语言后,我常常思考如何构建自己的编程语言,这也是我了解、尝试各种语言的动力之一。

编程语言是建立在抽象上的。从机器码到指令集,从汇编到高阶语言,从编译型到解释型,语言在抽象的同时也带来了巨大的开销。

解耦是编程语言中重要的一环。后文有着更深入的描述。

从编程语言哲学衍生出的抽象与解耦的概念,也是形成我的价值观的重要因素之一。我将抽象与解耦的 现实举例 放在了价值观页面,有兴趣可以跳转查看。

(狭义)抽象

我们将每一条指令、一个数据看作一个点。

函数

我愿意说函数是最早的抽象。函数 这个概念比 编程 出现还早得多,因此说底层也不足怪。现代处理器的指令集本身就可以看成是电路操作的函数。

很容易把函数想象成 pipe,一边输入,一边输出,因此函数是线级抽象。(真的吗?)

指针

指针是构成现代计算机的基础。由于指针没有权限控制,很容易写出不安全的代码,也徒增了许多心智负担。因此现代的语言都试图弱化指针在编程语言中的地位,将其抽象成什么别的东西。

C++ 的引用原先是指针的语法糖,后来添加了右值引用,事实上就是所有权的控制。

Java, JS, Python 等语言直接将指针的概念剥离了出去,使用较为简单粗暴的方式:基本类型都是传值,对象类型(包括其他复杂类型)都是传引用(指针)。

Rust 用引用的概念来代替指针,不过也保留了 unsafe 中的指针操作。要注意的是 C++ 的引用概念应该相当于 Rust 的 Box,而不是引用(ans:引用的引用?)。

结构

我原先认为结构是比函数高一阶的抽象(并且也这么写了),但事实上,如果说函数是指令的集合,结构是数据的集合,那么它们的阶数理应是相同的。如果函数与 OOP 的阶数差为 2,那么后文中的一些地方很难解释得通。因此先暂且(不严谨地)认为,结构和函数是同一级抽象。

C 的 struct & union 可以组合任意数据类型。由于 void* 的存在,什么 function pointer 啦都可以往 struct 里塞。

等等,这对吗?组合内可以有函数,函数的参数可以是组合,这其中的维度关系相当混乱。现在暂且把它当成抽象大厦上的一朵乌云~

OOP

最后才是 OOP 的天下。OOP 是比组合高阶的抽象(面级),优化了函数的组合语法,加了权限控制,继承关系等。其中的继承关系是关键,是抽象进阶的根源。

所有类(包括抽象类)如果是单继承的,那么继承关系构成一颗(许多棵)树。

interface & trait

至于 interface 和 trait 就更复杂了,我一开始还不太清楚它们的抽象层级。它们看起来花里胡哨,把自己叫成 implement[1] 而不是 inherit,实际上也可以直接看成多继承特性,交织的继承关系不再是树而是 DAG,但仍然没有维度上的突破。

相比之下,trait 要比 interface 灵活一点。

但 interface 和 trait 确实是同一个抽象层次的东西。

组合

常有人说,组合优于继承。一个现实的例子是,rust struct 无法继承,只能组合。

组合究竟是不是抽象的一个子类,我还没有搞清楚,这里先写着。

FP

现在还有很多函数式(Functional Programming)语言流行并活跃,比如 Ocaml, Haskell, Lisp, koka。FP 通过组合,得到高阶函数,完成复用。

FP 的基础是 lambda 演算,组合子[2]等。我自己也没有系统地学习过 FP(只看过一点点 SICP),这里就不献丑了。

monad

monad 是在函数式中的一个概念,可以理解成一种“包装”。Rust 中的 Option, Result 都是 monad。

有一些语言不喜欢 monad 的抽象。最容易看出来的就是用函数接受 default 参数的语言:

这样就会出现诸如求值顺序,多层嵌套难看等各种问题。而一个好的解法是将返回值抽象为 monad,例如 rust:

rust
fn xxx() -> Option<T> {}

Option 是一个 monad 抽象,其代表了“可能有值”。然后,再从 monad 中取出内部值:

rust
let a = xxx().unwrap_or_else(|| 1);

取值部分,可以设 default,可以立刻求值,可以懒求值,可以方便地嵌套,具有非常高的灵活性。

Algebraic Effect

代数效应是 FP 领域比较热门的一种抽象,它几乎是实现了 async、try-catch 等的大一统。代数效应的代表语言是 koka,并且 OCaml 5.0 起也支持了代数效应。

错误处理

目前编程语言的错误处理主要有两类,throw/catch 模型和 Result 模型。老牌语言更多使用前者,新兴语言更多使用后者。

throw/catch 看着非常像硬件中断。硬件中断会起一个错误处理线程来解决中断,并在解决后恢复(可恢复中断)。而 throw 的过程可以比作开启线程,catch 就是线程可执行部分。

理论上 throw/catch 应该算是普通的抽象。

Result 不是新兴语言的专利,C 语言的状态码也是 Result。非常明显,Result 是组合的那一类。

并发

目前大多数语言的并发基本都被 async/await 模型统一了。剩下 go 这种有栈协程还在支持。

async/await 模型是接近 FP 的体现,相当于把程序在 await 点切割,由一个/多个调度器控制程序调度。

解耦

(相关条目:杂论 - 解耦论)

已经有部分语言开始自发地(或被迫地)解耦。

module

模块化是很好的解耦实例。模块化把抽象出的结构包成黑箱,只留下外部 API 接口,能够方便代码复用,减轻程序员心智负担。

模块化的另一个关键是组合(FP 不请自来)。将小模块组合成大模块,提供更高层次的抽象。

例如 OOP 很好地实现了模块化的思想(权限控制)。

语言实现

特化

特化(specialization)是在面对多个泛型匹配项时,编译器根据内置优先级规则进行选择的行为。C++ 模板以复杂的特化闻名,甚至 Rust 也走上了特化的道路(min_specialization)。

但是我非常不喜欢特化,它极大增加了语言的复杂性,给人脑造成了不小的记忆负担。我倾向于类型集合论,如果类型系统是集合论完备的,并且规定同一个对象的所有泛型不允许交叉,那就不会出现特化的问题,匹配项可以由程序员决定。

细分

优先级,未完待续

梦中情语

on github (WIP)

external

  1. Lies we tell ourselves to keep using Golang
  2. A decade of developing a programming language
  3. What Color is Your Function?
  4. Object-Oriented Programming is Good*
  5. Coroutines and effects
  6. Null, Errors and Enums Are One Feature

  1. implement 有一种在《三体》"蓝色空间"号上通过四维空间碎片(高阶抽象)支配三维空间(低阶抽象)的感觉。 ↩︎

  2. 有一篇可以拿来当组合子快速入门的文章在 rust external articles 2.。 ↩︎