编程语言二三事

本文的写作动机:作为一般程序员而非专门的编程语言研究者,收集对于 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。

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *