变量与作用域

3.1 简介

在上一章中,我们实现了一个简单的计算器。为了让它更像一门编程语言,我们还需要添加三个方面的内容:

  1. 1. 变量。用于操作状态。
  2. 2. 控制流。比如条件语句和循环语句。
  3. 3. 函数。用于代码复用。

以下是一些示例程序,展示了我们这门语言的语法:

;; 定义函数 `fib`,带有一个参数 `n`
(def fib (n)
    ;; 条件判断语句(if-then-else)
    (if (le n 0)
        (then 0)
        (else (+ n (call fib (- n 1))))))   ;; 函数调用

(call fib 5)
;; `fib` 函数的另一个版本
(def fib (n) (do
    (var r 0)           ;; 变量声明
    (loop (gt n 0) (do  ;; 循环
        (set r (+ r n)) ;; 变量赋值
        (set n (- n 1))
    ))
    (return r)          ;; 从函数调用中返回
))

(call fib 5)

3.2 用于变量的新命令

一个程序是一系列操作状态的操作序列,所以我们将添加一个新的结构来执行一系列操作。do 命令会按顺序计算其参数,并返回最后一个参数。它还会为变量创建一个新的 “作用域”。

(do a b c...)

并且我们添加了两个新的命令,用于变量声明和赋值。

(var a init)    ;; 创建一个带有初始值的新变量
(set a value)   ;; 为变量赋一个新值

我还在这门语言中添加了注释。分号会忽略该行的剩余部分。通过扩展 skip_space 函数来处理注释:

def skip_space(s, idx):
    while True:
        save = idx
        # 尝试跳过空白字符
        while idx < len(s) and s[idx].isspace():
            idx += 1
        # 尝试跳过单行注释
        if idx < len(s) and s[idx] == ';':
            idx += 1
            while idx < len(s) and s[idx] != '\n':
                idx += 1
        # 没有更多的空白字符或注释
        if idx == save:
            break
    return idx

3.3 变量与作用域

变量是有作用域的 —— 它们只能被其同级表达式访问。在计算表达式时,我们将使用一个映射来存储变量。

问题在于,子表达式可能会定义与父作用域中变量名冲突的变量。通过使用每个作用域的映射,而不是全局映射来解决这个问题。

pl_eval 函数获得了一个新的参数 env,用于存储变量。

def pl_eval(env, node):
    # 读取一个变量
    if not isinstance(node, list):
        assert isinstance(node, str)
        return name_loopup(env, node)[node]
   ...

env 参数是一个链表,它包含当前作用域的映射以及指向父作用域的链接。进入和离开一个作用域,只需添加或删除链表头部。

变量查找函数会向上遍历链表,直到找到变量名。

def name_loopup(env, key):
    while env:  # 链表遍历
        current, env = env
        if key in current:
            return current
    raise ValueError('未定义的名称')

计算新作用域的代码:创建一个新的映射,并将其链接到当前作用域。then 和 else 命令是 do 命令的别名。它们只是语法糖,这样你就可以写成 (if (then xxx) (else yyy)) 而不是 (if xxx yyy)

def pl_eval(env, node):
   ...
    # 新作用域
    if node[0in ('do''then''else'and len(node) > 1:
        new_env = (dict(), env) # 将映射添加为链表头部
        for val in node[1:]:
            val = pl_eval(new_env, val)
        return val  # 最后一个元素

现在,var 和 set 命令的代码就很直观了。

def pl_eval(env, node):
   ...
    # 新变量
    if node[0] == 'var' and len(node) == 3:
        _, name, val = node
        scope, _ = env
        if name in scope:
            raise ValueError('名称重复')
        val = pl_eval(env, val)
        scope[name] = val
        return val
    # 更新变量
    if node[0] =='set' and len(node) == 3:
        _, name, val = node
        scope = name_loopup(env, name)
        val = pl_eval(env, val)
        scope[name] = val
        return val

3.4 测试

解释器接受一系列表达式,而不是单个表达式。这是通过用 do 命令包装输入来实现的。

def pl_parse_prog(s):
    return pl_parse('(do'+ s + ')')

一个展示变量和作用域的示例程序:

def test_eval():
    def f(s):
        return pl_eval(None, pl_parse_prog(s))

    assert f('''
        ;; 第一个作用域
        (var a 1)
        (var b (+ a 1))
        ;; a=1, b=2
        (do
            ;; 新作用域
            (var a (+ b 5))     ;; 名称冲突
            (set b (+ a 10))
        )
        ;; a=1, b=17
        (* a b)
    ''') == 17

我们将在下一章中添加控制流和函数。

阅读剩余
THE END