本文的写作动机:作为一般程序员而非专门的编程语言研究者,收集对于 PL 的一些基础概念,作此草稿。本文较为琐碎,章节标题并列不代表这两个话题一定是并列的关系。
更新策略:直接修改本文内容
作用域、名称绑定和闭包
自由变量 (Free) 和约束 (Bound) 变量
在数理逻辑中
在表达式 Sigma_{k=1}^10 f(n, k)
中,n 是自由变量,k 是约束变量。
最终表达式的值依赖于变量的值,那么变量是自由变量;变量名可以被随意替换而不影响外部性质,那么变量是约束变量。
在编程语言中
自由变量是指函数作用域中既不是局部变量,又不是函数参数的变量。
作用域与名称绑定 (Name Binding)
名称绑定将名字与实体相对应。作用域决定变量的绑定方式。
静态作用域(词法作用域)Static scope (Lexical scope) 有一个编译时静态确定的作用域(如一个函数或一个块),变量在区域内可见,这是大多数编程语言(C++, Python, Java, …)采用的规则。
动态作用域 Dynamic Scope 维护一个运行时才确定的名称绑定栈 (binding stack),程序只要在执行动态变量的代码段,那么该变量一直存在。例如,若函数 f
里面调用了函数 g
,那么在执行 g
的时候,f
里的所有局部变量都会被 g
访问到。Commom Lisp 和 Perl 使用动态作用域。
例如,下面的例子中,使用静态作用域规则会打印 1,使用动态作用域规则会打印 2。
x = 0
def f(y):
return x + y
def g(z):
x = 1
return f(z)
g(1) # evaluates to 1 or 2?
绑定有 Early (Static) Binding vs Late (Dynamic) Binding 之分,顾名思义。它大部分由作用域规则决定,但使用静态作用域的 C++ 语言也可以使用晚绑定:C++ virtual method call 就是晚绑定。
闭包 (Closure)
闭包是头等函数(first-class function,即函数被当作头等公民,如可以作为变量、返回值)的编程语言中,使用词法绑定的方法。我将闭包简单理解为打包了环境的函数。“环境”其实就是自由变量,自由变量可能以值或者引用(具体取决于编程语言)的方式绑定。
Python 闭包例子
def foo():
x = 3
def bar():
print(x)
x = 5
return bar # returns a closure
f = foo()
f() # 5
上面的例子中,foo 返回的是一个闭包,闭包函数是 bar,自由变量是 x。
C++ 闭包例子
std::vector<int> some_list{ 1, 2, 3, 4, 5 };
int total = 0;
int value = 5;
std::for_each(begin(some_list), end(some_list), [&total, value, this](int x) { total += x * value * this->some_func(); });
这里的匿名函数就是闭包。自由变量 total 以引用的方式传入闭包内部;value, this 以值的方式传入闭包内部。
值语义和对象语义
值语义
值语义:Value Semantics。以 C++ 语言为例说明:执行赋值操作时,如果左操作数是新建对象,那么系统会调用拷贝构造函数,否则系统调用赋值操作符函数。拷贝的是属性值。拷贝前后,副本和原数据互不干扰。
class A{
public:
int value;
A(int v): value(v) {}
void print(){cout << value << endl;}
};
int main(){
A obj1(1);
A obj2 = obj1;
obj1.value = 2;
obj1.print(); // 2
obj2.print(); // 1
}
对象语义
对象语义 Object Semantics / 引用语义 Reference Semantics:指 Python, Java, C#, JavaScript 等语言中的 Object 都是指针,拷贝时并不拷贝内容。如下面的 Java 例子中,a 和 b 指向同一个 ArrayList 对象。
ArrayList<Integer> a = new ArrayList<Integer>();
ArrayList<Integer> b = a; // a 和 b 指向同一个 ArrayList 对象
a.add(10);
System.out.println("After modifying 'a', the object is: " + a); // [10]
System.out.println("Since 'b' references the same object, 'b' is also: " + b); // [10]
System.out.println(a == b); // true
在值语义和对象语义间切换
只举 Java 一个例子。在 Java 中,原始类型的赋值采用值语义,其余变量皆对象,赋值使用对象语义。如果想对 Java 的 Object 实现值语义,可以用 Project Lombok’s 的 @Value
修饰符。
编译器和运行时系统
区分编程语言和编程语言的实现
首先,编程语言只是定义计算机程序的抽象和规约,并没有规定它怎样实现(通过编译还是解释来实现?没有规定)(甚至一个语言对应多种实现,如 Python 就有 CPython, PyPy 等实现)因此,“编译型”语言和“解释型”语言的界限并不分明。
依据编程语言既有实现的通用特征,我们可以将编程语言模糊地分为两类:
- 非托管语言(Unmanaged Languages),这类语言通常采用 “静态编译到架构相关的机器码,高效执行” 的方式实现。如 C, C++, Rust。
- 托管语言(Managed Languages),这类语言通常采用 “保持级别、架构无关的格式,在运行时系统中执行” 的方式实现,如 Java, Python。
谈谈运行时系统中的 JIT
运行时系统涉及很多话题,典型地如垃圾收集等等,在此只提一嘴即时编译(Just-in-time compilation)的概念。即时编译,即在执行过程中进行源代码或字节码到机器码的转换,直接运行机器码来提升性能。一些常用的运行时系统中,Java 虚拟机采用 JIT,CPython 不包含 JIT,PyPy 包含 JIT。
利用中间语言设计编译器
下面的方法似乎是实现编程语言(具体来说,构建编译器)的典型方法:
- 定义一个中间语言 (IL: Intermediate Language / IR: Intermediate Representation)
- 用一个编译器前端将多种语言翻译为中间语言
- 用一系列运行时环境运行中间语言,或者用一系列编译器后端将中间语言翻译为机器码
微软的 CIL 和 CLR
微软通过通用中间语言(Common Intermediate Language, CIL,亦称 MSIL)作为桥梁实现了诸多编程语言:这些编程语言 C#, F#, VB.NET, C++/CLI 都可以编译到 CIL;CIL 可以在 CLR (Common Language Runtime) 虚拟机上运行。
微软开发的 CLR 叫做 .NET 运行时。.NET 早期并不开源, Unity 需要用 C# 开发,所以自己另搓了一个名为 Mono 的 CLR。.NET 开源后,Unity 采用了 .NET。
LLVM
LLVM 是围绕一个 IR 标准建立的编译器工具链。可以用它来在任何平台上实现任何编程语言:首先用前端把目标编程语言编译到 LLVM IR,然后利用后端将 IR 翻译为目标平台上的机器码。
LLVM 的应用很广泛:Rust 的编译器 rustc 干的事情就是将 Rust 程序翻译为 low-level LLVM IR,然后调用 LLVM 后端生成目标机器码。LLVM 开发小组推出的 Clang 编译器也是一个前端,可以将 C, C++, Objective-C, Objective-C++ 翻译为 LLVM IR。
Leave a Reply