从名字开始:变量和 let

本节阅读量:

写第二章代码之前,先把语言范围说清楚。

第一章的表达式只有两种:

1
2
整数
加法

第二章增加两种:

1
2
变量引用
let 绑定

也就是说,本章仍然是“一个程序就是一个表达式”,只是表达式种类更多了。

变量引用

变量是源码里的名字,例如:

1
2
3
x
answer
total

变量引用本身不保存值。它只是说:

1
到当前环境里找这个名字。

这个程序可以得到 40

1
(let x 40 x)

因为 body 里的 x 可以看到外层 let 建立的绑定。

但这个程序会报错:

1
x

语法上它是一个合法变量引用;求值或编译时才会发现没有任何地方绑定了 x

let 表达式

本章的 let 写成:

1
(let x 40 (+ x 2))

它由六个小块组成:

1
2
3
4
5
6
(       左括号
let     特殊形式
x       要绑定的名字
40      value,先求出这个值
(+ x 2) body,在这里使用 x
)       右括号

可以读成:

1
在 body 里,让 x 表示 value 的结果。

所以:

1
(let x 40 (+ x 2))

结果是:

1
42

value 先在旧环境里求值

let 的 value 也可以是表达式:

1
(let x (+ 20 20) (+ x 2))

先算 value:

1
(+ 20 20) -> 40

再用扩展后的环境求 body:

1
(+ x 2) -> 42

这里有一个容易写错的规则:

1
(let x 1 (let x (+ x 1) x))

内层 let 的 value 是:

1
(+ x 1)

这个 value 要在旧环境里求值,所以这里的 x 仍然是外层的 1。算出 2 以后,内层 body 里的 x 才绑定到新的 2

同名遮蔽

如果内层又绑定同名变量,内层绑定会暂时遮住外层绑定:

1
(let x 10 (+ (let x 32 x) x))

内层:

1
(let x 32 x)

结果是 32

离开内层以后,外层 x 仍然是 10。所以整个程序是:

1
32 + 10 = 42

这叫 shadowing。它不是修改外层变量,而是在更小的范围里建立了一个新的同名绑定。

写成规则

第二章的表达式规则是:

1
2
3
4
5
6
7
expr ::= integer
       | identifier
       | "(" "+" expr expr ")"
       | "(" "let" identifier expr expr ")"

integer    ::= "-"? digit+
identifier ::= letter+

本章先把 identifier 规则定得很简单:

1
一个或多个字母

所以这些名字都可以作为普通变量名:

1
2
3
x
answer
total

x1total-1done? 这类名字暂时不支持。+ 仍然是加法操作符,不是普通变量名。

合法和非法

合法程序:

1
2
3
4
5
6
7
42
-7
x
(+ 40 2)
(+ (+ 1 2) (+ 3 4))
(let x 40 (+ x 2))
(let x 10 (+ (let x 32 x) x))

其中 x 只是语法合法;如果没有绑定,运行时仍然会报错。

非法程序:

1
2
3
4
5
6
(let x)
(let x 1)
(let 1 40 1)
(+ 1)
(+ 1 2 3)
(let x 1 2 3)

原因也很直接:

1
2
3
4
let 后面必须有名字、value、body。
绑定名必须是 identifier。
+ 后面必须正好有两个表达式。
完整程序只能有一个表达式。

现在已经知道第二章什么文本算合法。下一节先看 AST 怎样表示变量和 let


2.0 无名,天地之始;有名,万物之母

上一节

2.2 AST:给名字留位置

下一节