编程语言的本质是什么

5,263 阅读9分钟

作为程序员,我们会接触到各种各样的语言:

我们会用 Javascript、Typescript 来写前端应用,用 Java、Go 等来写后端应用,也会用 Python 来写一些工具脚本。

每种语言都有自己的语法和擅长的领域,那不同的编程语言的区别是什么呢?编程语言的本质是什么呢?

这篇文章我们尝试探究一下。

从硬件到语言

不同的语言最终都是控制计算机的一些硬件来工作的,从硬件层面来看他们是没有区别的。各种语言只不过描述逻辑的方式、 api 的封装方式不同而已,底层都跑在同一套硬件上。

那硬件层面做了啥呢?

就拿打印机来说,它本身是一个机械的结构,但是控制机械工作的确是通过电子的方式,也就是 0 和 1 的电信号,一般控制它工作的芯片都有不同的引脚,传高低电平代表不同的含义,那么就可以通过高低电平来控制打印机做不同的工作。

控制硬件工作的程序就叫做驱动程序,它把硬件的能力做了封装,提供了各种 api。这样,我们就可以通过程序控制各种硬件了。

这些硬件中最特殊的是 CPU,其他硬件提供的指令都是控制该硬件工作的,而 CPU 提供的指令确是可以描述各种逻辑,可以读写内存,进而控制其他硬件。

这样我们就不用直接控制其他硬件,而是可以通过 CPU 来间接的控制各种硬件,所以,它叫做中央处理器。这些机器指令叫做指令集,由它所描述的逻辑,就是机器语言。

硬件是通过电子来控制机械,提供了驱动程序,然后又通过 CPU 来实现各种通用的逻辑,进而控制其他硬件。CPU 提供的指令集所描述的逻辑,就叫做机器语言,这是我们写的程序最底层的样子。

为什么要有操作系统

计算机上肯定不能只跑一个程序,那是最早的计算机,现在的计算机都是支持多个程序的并发的。但是不管跑了多少了程序,都是在同一个硬件上跑,只是做了顺序、优先级等的安排,这种叫做调度,实现多个程序的调度的程序就是操作系统。

既然是为了让多个程序都能跑在同一个硬件上,那得先给这个跑起来的程序一个名字,就叫做进程。进程调度是 CPU 最主要做的事情。进程要用各种硬件,那也就还需要内存调度、I/O 调度等。

总之,操作系统主要做的事情就是支持了程序的并发执行,把各种资源统一管理了起来,实现了进程、内存、IO 等等调度。

同时,为了安全性,操作系统会把程序的运行状态分为用户态和内核态,只有内核态可以访问驱动,来控制硬件,然后提供了系统调用给用户态来用,因为如果任何程序都能随意操作硬件,那就不安全了,所以要管控起来。

为什么讲编程语言会讲到操作系统呢?

因为我们写的应用层的代码都是在操作系统上跑的,用的各种 api 也最终都是操作系统提供的系统调用来实现的。

但我们不是直接使用系统调用,而是用各种语言的标准库,这些标准库就是对系统调用做了进一步的封装,比如创建进程、访问网络、访问内存等等。Node.js 的 api、JDK 的 api 都是基于系统调用封装的。

操作系统实现了多个程序在同一套硬件上的并发执行,为了安全,还把程序的运行分为了内核态和用户态,提供了系统调用来使用操作系统能力,各种语言对系统调用做了封装,这样,我们就能通过这些 API 来控制计算机了。

编程范式与描述方式

我们讲了如何通过机器语言来控制 CPU 进而控制其他硬件,讲了操作系统的功能和它提供的系统调用是怎么被编程语言封装的,这些都是我们能够控制计算机的基础。

但是我们现在还停留在机器语言呢,用这个来写逻辑也太麻烦了,既要考虑怎么表达逻辑,又要考虑计算机是怎么执行的,比如要访问那个寄存器、读写哪个内存等。

能不能简化一些呢?

首先想到的是把机器语言做成一些有含义的字符串,叫做汇编语言,这样描述起来就简单很多。

但是这依然要考虑表达的逻辑、计算机执行细节这两个方面,而执行细节其实与逻辑没关系,而且不同机器和操作系统的执行细节也可能不同。

能不能我只管怎么表达逻辑,然后通过一种方式来转成带有执行细节的机器语言呢?

这种就是高级语言了,它的特点就是没有具体的执行细节,只关心逻辑的表达,实现这种转换的就是编译器。(当然,也可以做成一个解释执行其他程序的中间程序,叫做解释器)

而描述逻辑这件事情有不同的方式,比如我可以通过一个个函数来组织逻辑,把数学那套思维拿过来,这叫函数式,也可以通过一个个对象来组织逻辑,这叫做面向对象,这些不同的描述逻辑的思路就是编程范式。

编程语言主要就是实现了某几种编程范式,这样,程序员就可以通过不同的方式来描述逻辑,由编译器去转成带有计算机执行细节的机器代码。

不同语言实现的编程范式不同,也就是描述逻辑的方式不同,这是语言之间最大的区别。 至于能做什么,这个不是区别,只要对系统调用封装一下,做成一些库就可以支持。

比如 Javascript 最开始只可以在浏览器上跑,描述渲染逻辑,但后来有了 Node.js 后,它同样可以用来描述一些脚本或者服务端逻辑。

像现在的跨端引擎,不就是对操作系统能力做了封装,通过 Js 来描述逻辑,然后由 native 来调用操作系统能力么?

还有 electron、hybrid 等等,这些都是 Javascript 的 runtime,他们扩展的是 api,并没有扩展 js 语言本身。

那什么扩展了 Javascript 语言本身呢?是 Typescript 这种编译成 Javascript 的语言,它提供了类型系统,可以静态检查出程序中的一些错误。

为了分离逻辑的表达和程序执行的细节,我们实现了高级语言,这样程序员只需要专注逻辑的表达,然后通过编译器/解释器来转换成带有执行细节的机器语言代码。而逻辑表达有不同的方式,比如面向对象、函数式等,每种编程语言会实现其中的几种,这是语言之间最大的区别。语言只是表达逻辑用的,至于能做什么,则是 api的事情,只要对系统能力做下封装,就可以扩展其他的 api,进而可以写该领域的逻辑,比如 Node.js、Electron、跨端引擎等都是 api 的扩展。

总结

我们从硬件、操作系统、编程范式三个层次来探讨了编程语言的本质:

  • 硬件是用电子控制机械,通过驱动程序来驱动硬件工作,而 CPU 可以描述通用的逻辑,进而控制其他硬件,我们就是通过控制 CPU 来间接控制各种硬件的,所以它叫做中央处理器。它提供的指令集所表达的逻辑叫做机器语言。

  • 操作系统实现了程序的并发执行,让一套硬件上可以同时跑多个程序,叫做进程。操作系统支持了进程、内存、IO 等各种调度。为了安全,把程序的执行分成了用户态和内核态两个状态,内核态才可以通过驱动控制硬件,然后把它做成了系统调暴露给用户态。各种语言的标准库就是通过系统调用来使用操作系统的能力的。

  • 机器语言是我们控制计算机最根本的方式,但是它要逻辑的表达和计算机执行细节两方面,为了分离两者,我们做了编译器/解释器来完成这种转换,这样我们只需要表达逻辑即可,这叫做高级语言。描述逻辑有不同的方式,叫做编程范式,每种编程语言都实现了某几种编程范式。不同编程语言的区别只是表达逻辑的方式不同,至于可用的 api,这个可以通过库或者 runtime 来扩展。

所以,如果让你做一门编程语言,你要做什么呢?

  • 你要先选择一种编程范式,用它来表达逻辑,然后要设计细节的语法。

  • 之后实现编译器/解释器来让它能够转成控制计算机运行的机器语言。

  • 之后要实现对操作系统能力做了封装的标准库。

  • 然后如果你要表达不同领域的逻辑,则要实现不同领域的一些库,比如图形领域的、桌面端的、web 服务器的等等。

这是实现编程语言的思路,也是我们理解编程语言的思路。