受限计算模型
学习目标
- 认识到图灵完备性并非总是好事
- 另一些总是停机的形式系统的例子: 上下文无关文法(context-free grammars)和简单类型λ演算
- 非上下文无关函数(non context-free functions)的泵引理(pumping lemma)
- 正则表达式和上下文无关文法的可计算与不可计算的语义属性(semantic properties)示例
“幸福的家庭都是相似的, 不幸的家庭各有各的不幸.”
——Leo Tolstoy(《安娜·卡列尼娜》开篇)
我们已经看到, 许多计算模型都是图灵等价的, 包括图灵机, NAND-TM/NAND-RAM程序, C/Python/JavaScript等标准编程语言, 以及演算乃至生命游戏等其他模型. 其反面则是, 对于所有这些模型, Rice定理(定理9.7)同样成立, 这意味着此类模型中程序的任何语义属性都是不可计算的.
对于图灵等价模型, 停机问题及其他语义判定问题的不可计算性, 促使我们去研究受限的计算模型(restricted computational models). 这些模型需要满足: (a) 足够强大, 能够捕获对某些应用有用的一类函数; (b) 足够受限, 使得我们仍然能够解决其上的语义判定问题. 本章将讨论几个这样的例子.
图灵完备性之弊
我们已经看到, 看似简单的计算模型或系统可能被证明是图灵完备的. 下面这个网页列举了若干“意外“实现图灵完备的形式系统案例, 包括本应受限的语言, 如C预处理器, CSS, (某些变体的)SQL, sendmail格局, 以及《我的世界》《超级马力欧》和卡牌游戏《万智牌》等游戏. 图灵完备性并非总是好事, 因为它意味着这类形式系统可能产生任意复杂的行为. 例如, PostScript格式(PDF的前身)本是一种用于描述打印文档的图灵完备编程语言. PostScript强大的表达能力可以用简短代码描述极其复杂的图像, 但也曾引发棘手问题——正如该网页所述, 攻击者既能利用无限循环实施拒绝服务攻击, 也能借此访问打印机文件系统.
图灵完备性的陷阱的一个近期典型案例出现在加密货币以太坊中. 该货币的核心特点在于能够使用表现力强(特别是图灵完备)的编程语言设计“智能合约“(smart contracts). 在我们当前“人力运作“的经济体系中, Alice和Bob可以签订合约, 约定若条件X发生则共同投资查理的公司. 而以太坊允许双方创建合资项目: 将资金汇入由程序监管的账户, 该程序决定在何种条件下拨付款项. 例如, 可以设想一段在Alice, Bob和在Bob汽车运行的程序之间交互的代码, 使得Alice能在无人干预或管理的情况下租用Bob的汽车.
具体而言, 以太坊采用的图灵完备编程语言Solidity具有类JavaScript语法. 其标志性实验是一个名为“去中心化自治组织“(The DAO)的项目, 旨在创建无需人类管理者, 由智能合约自主运营的去中心化风投基金, 股东可共同决策投资机会. DAO曾是历史上最成功的众筹项目, 巅峰时期市值达1.5亿美元, 占当时以太坊市场总值的10%以上. 投资DAO(或参与任何其他“智能合约“)等同于将资金交由计算机程序管理, 即“代码即法律“, 或用DAO的自我描述: “DAO诞生于不可篡改, 不可阻挡, 无可争议的计算机代码”.
然而(正如第9章所述), 理解程序行为本质上是极其困难的. 一名黑客(亦有人称之为精明的投资者)构造了特殊输入, 使DAO代码陷入无限递归循环, 持续将资金转入其账户, 最终盗取约6000万美元. 虽然该交易符合智能合约代码规范而在技术层面“合法“, 但显然违背了代码编写者的本意. 以太坊社区对此事件的应对陷入困境: 有人尝试“劫富济贫“式的操作, 利用相同漏洞将DAO资金转移至安全账户, 但收效甚微. 最终社区达成共识——代码必须可修正, 可中止, 可反驳. 以太坊维护者与矿工通过“硬分叉“(hard fork, 亦称作“紧急救助“bailout)方案将历史记录回滚至黑客交易发生前. 部分社区成员强烈反对该决议, 由此衍生出保留原始记录的新币种“经典以太坊“(Ethereum Classic).
上下文无关文法
如果你写过程序, 那么一定遇到过语法错误. 你可能也经历过程序陷入无限循环的情况. 但编译器或解释器在判断程序是否有语法错误时陷入无限循环的可能性则小得多.
当人们设计一门编程语言时, 需要确定它的语法(syntax). 也就是说, 设计者要决定哪些字符串对应有效的程序, 哪些不是(即哪些字符串包含语法错误). 为了确保编译器或解释器在检查语法错误时总能停机, 语言设计者通常不会使用图灵完备的通用机制来表达语法, 而是采用一种受限的计算模型. 其中最常见的模型之一就是上下文无关文法.
为了解释上下文无关文法, 让我们从一个经典例子开始. 考虑函数 它以字母表上的字符串作为输入, 当且仅当字符串表示一个有效的算术表达式时返回 直观上, 我们通过将 或等运算作用于较小的表达式, 或者用括号将其括起来, 来构建表达式, 其中“基本情况“对应那些只是数字的表达式. 更精确地说, 我们可以作出以下定义:
- 一个数字是符号之一.
- 一个数是一个数字序列. (为简化起见, 我们忽略了序列不能有前导零的条件, 尽管在上下文无关文法中编码此条件也不难)
- 一个运算符是 之一.
- 一个表达式的形式可以是“数“, “子表达式1 运算符 子表达式2“或”(子表达式1)“, 其中“子表达式1“和“子表达式2“本身也是表达式. (注意, 这是一个递归定义)
上下文无关文法(CFG)是表达此类条件的一种形式化方法. CFG由一组规则构成, 这些规则告诉我们如何从较小的组成部分生成字符串. 在上面的例子中, 其中一条规则是“如果和是有效表达式, 那么也是有效表达式“;我们也可以使用简写来书写这条规则. 正如上例所示, 上下文无关文法的规则通常是递归的: 规则使用其自身来定义有效表达式. 我们现在正式定义上下文无关文法:
人们使用许多不同的记法来书写上下文无关文法. 最常见的记法之一是Backus-Naur范式(BNF). 在这种记法中, 我们将(其中是变量, 是字符串)形式的规则写为<v> := a. 如果我们有 等多条规则, 则可以将它们合并为<v> := a|b|c. (换句话说, 我们称可以导出出 或例如, 示例10.2的上下文无关文法的Backus-Naur描述如下(使用ASCII字符表示运算符):
operation := +|-|*|/
digit := 0|1|2|3|4|5|6|7|8|9
number := digit|digit number
expression := number|expression operation expression|(expression)
上下文无关文法的另一个例子是“匹配括号“文法, 它可以用Backus-Naur范式表示如下:
match := ""|match match|(match)
当且仅当一个由字母表构成的字符串(其中match是起始表达式, ""对应空字符串)可以由该文法生成时, 它才由一组匹配的括号组成. 相反, 根据引理6.2, 不存在这样的正则表达式: 它能匹配字符串当且仅当包含一个有效的匹配括号序列.
作为计算模型的上下文无关文法
我们可以将字母表上的上下文无关文法视为定义了一个函数, 该函数根据字符串是否能由文法规则生成, 将中的每个字符串映射为或 现在我们将此定义形式化.
若是上的一个上下文无关文法, 则对于两个字符串 如果我们可以通过应用的一条规则从得到 则称可从一步导出(derived in one step), 记作 也就是说, 我们通过将中一个变量的出现替换为字符串来得到 其中是的一条规则.
如果可以通过有限步数从导出, 则称可从导出(derive), 记作 也就是说, 如果存在 使得
如果可以从起始变量导出(即如果 则称被匹配(matched). 我们将由计算的函数定义为映射 使得当且仅当被匹配. 如果对于某个CFG有 则函数是上下文无关(context free)的. 1
先验地看, 映射是否可计算可能并不明显, 但事实确实如此.
与往常一样, 我们只关注上的文法, 尽管下列证明可以推广到任何有限字母表
定理 10.1的证明
定理 10.1的证明
我们仅概述证明. 首先观察到, 我们可以将每个CFG转换为等价的Chomsky范式, 其中所有规则要么具有的形式(为变量), 要么具有的形式(为变量, 另外可能还有规则 其中是起始变量.
这种转换背后的思想是简单地根据需要添加新变量, 例如, 我们可以将这样的规则转换为三条规则: 和
使用Chomsky范式, 我们得到一个自然的递归算法, 用于计算对于给定的文法和字符串 是否有 我们只需尝试所有可能作为该导出中使用的第一条规则的猜测, 然后尝试所有可能将划分为连接的方式. 如果我们正确地猜测了规则和划分, 那么我们的任务就简化为检查是否和 这(由于涉及更短的字符串)可以通过递归完成. 基本情况是当为空或单个符号时, 可以轻松处理.
虽然我们专注于判定CFG是否匹配字符串这一任务, 但计算的算法实际上提供了比这更多的信息. 也就是说, 在输入字符串时, 如果 则算法会生成可以从起始顶点应用以获得最终字符串的规则序列. 我们可以将这些规则视为确定了一棵树, 其中是根顶点, 而汇点(或叶子)对应于通过那些第二个元素中不含变量的规则获得的的子串. 这棵树被称为的解析树(parse tree), 通常能提供关于结构的非常有用的信息.
编程语言的编译器或解释器的第一步通常是解析器(parser), 它将源代码转换为解析树(也称为抽象语法树(abstract syntax tree)). 也有一些工具可以自动将上下文无关文法的描述转换为能计算给定字符串解析树的解析器算法. (事实上, 上述递归算法可用于实现此目的, 但存在更高效的版本, 特别是对于具有特定形式的文法, 而编程语言设计者通常会努力确保其语言具有这些更高效的文法)
上下文无关文法的表达能力
上下文无关文法可以描述所有正则表达式:
定理 10.2的证明
定理 10.2的证明
我们通过对正则表达式的长度进行归纳来证明此定理. 如果是长度为1的表达式, 则或 在这种情况下, 存在一个(平凡的)CFG可以计算它, 我们将其留给读者验证. 否则, 我们将落入以下三种情况之一:
- 情况1:
- 情况2:
- 情况3:
在所有这些情况下, 和都是更短的正则表达式. 根据归纳假设, 我们可以分别定义计算和 的文法和 通过重命名变量, 我们也可以不失一般性地假设和是不相交的.
在情况1中, 我们可以按如下方式定义新文法: 添加一个新的起始变量以及规则 在情况2中, 我们可以按如下方式定义新文法: 添加一个新的起始变量以及规则和 情况3是唯一使用递归的情况. 和前面一样, 我们添加一个新的起始变量 但现在添加规则(即空字符串), 并且对于中每一条形式为的规则, 将规则添加到中.
我们将其作为(一个非常好的!)练习留给读者, 去验证在所有三种情况下, 我们生成的文法所描述的语言与原正则表达式描述的语言相同.
事实证明, 上下文无关文法严格比正则表达式更强大. 特别地, 正如我们所见, “匹配括号“函数可以由一个上下文无关文法计算, 而根据引理6.2, 它不能由正则表达式计算. 下面是另一个例子:
令为在练习6.4中定义的函数, 其中当且仅当具有形式时, 那么可以由一个上下文无关文法计算.
对练习 10.1的解答
对练习 10.1的解答
可以使用Backus-Naur范式描述一个计算的简单文法:
start := ; | 0 start 0 | 1 start 1
可以通过归纳法证明, 该文法生成的恰好是满足的字符串
一个更有趣的例子是计算那些形式为 但不是回文的字符串:
对练习 10.2的解答
对练习 10.2的解答
使用Backus-Naur范式, 我们可以将这样的文法描述如下:
palindrome := ; | 0 palindrome 0 | 1 palindrome 1
different := 0 palindrome 1 | 1 palindrome 0
start := different | 0 start | 1 start | start 0 | start 1
换句话说, 这意味着我们可以将满足的字符串描述为具有以下形式:
其中是任意字符串, 且 因此, 我们可以先生成一个回文(通过palindrome变量), 然后在左边或右边添加 并在对侧添加 从而得到一个非回文(通过different变量), 接着我们可以在任一端添加任意数量的和(通过start变量).
上下文无关文法的局限性(可选)
尽管上下文无关文法比正则表达式更强大, 但仍有一些简单语言无法被上下文无关文法描述. 证明这一点的一个工具是“泵引理“的上下文无关文法版本(另见定理6.8):
定理 10.3的证明
定理 10.3的证明
我们只概述证明思路. 其思想是: 如果文法规则中的符号总数为 那么要得到满足且的 唯一的方法就是使用递归. 也就是说, 必须存在某个变量 使得我们能够从推导出形如的串(其中 并且随后还能从推导出某个串 使得是的一个子串(换句话说, 对于某些 有 如果我们选择满足此要求且推导步骤数最少的变量 那么可以确保至多是某个依赖于的常数, 我们可以设为该常数(即可, 因为我们最多需要次规则应用, 而每次这样的应用最多能将字符串增长个符号).
因此, 根据文法的定义, 我们可以重复推导过程, 将中的子串替换为(对于每个 同时保持的输出仍为1. 由于是的子串, 我们可以记 并保证对于每个 都能被该文法匹配.
利用定理 10.3, 我们可以证明即使是下面定义的简单函数 也不是上下文无关的. (相比之下, 函数定义为: 当且仅当对于某个和 有 这个函数是上下文无关的. 你能否看出原因?)
对练习 10.3的解答
对练习 10.3的解答
上下文无关语言的语义性质
与正则表达式的情况类似, 上下文无关文法的局限性确实带来某些优势. 例如, 上下文无关文法的空性问题是可判定的:
定理 10.4的证明思路
定理 10.4的证明思路
若将文法转换为定理 10.1中的乔姆斯基范式, 则证明更易理解. 给定文法 我们可以递归地定义一个非终结符变量为非空, 如果存在形如的规则, 或者存在形如的规则, 其中和均为非空. 则该文法非空当且仅当起始变量非空.
定理 10.4的证明
定理 10.4的证明
假设文法为定理 10.1中的乔姆斯基范式. 我们考虑以下将变量标记为“非空”的过程:
- 首先, 将所有涉及形如规则的变量标记为非空.
- 随后, 若变量涉及形如的规则, 且和已被标记过, 则继续将标记为非空.
重复此过程, 直到无法再标记任何变量. 此时, 当且仅当未被标记, 我们判定该文法为空. 要理解该算法的有效性, 请注意: 若变量被标记为“非空”, 则存在某个字符串可从推导出. 反之, 若未被标记, 则从出发的每个推导序列始终包含未被字母表符号替换的变量. 因此, 特别地, 当且仅当起始变量未被标记为“非空”时, 是恒零函数.
上下文无关文法等价性的不可计算性(选读)
与正则表达式类比, 人们曾希望找到一种算法, 用于判定两个给定的上下文无关文法是否等价. 但遗憾的是, 我们并没有这样的好运. 事实证明, 上下文无关文法的等价性问题是不可计算的. 这是以下定理的直接推论:
定理 10.5 立即意味着上下文无关文法的等价性是不可计算的, 因为在某个字母表上计算文法的“满性“, 就对应于检查是否等价于文法 请注意, 定理 10.5和定理 10.4一起意味着, 与正则表达式不同, 上下文无关文法在补集运算下不封闭. (你能看出原因吗?)由于我们可以用位来编码中的每个元素(并且这种有限编码可以很容易地在文法中实现), 定理 10.5意味着, 对于二进制字母表上的文法, 满性同样是不可计算的.
定理 10.5的证明思路
定理 10.5的证明思路
我们通过从停机问题归约来证明该定理. 为此, 我们使用NAND-TM程序的格局这一概念, 如定义8.3所定义. 回想一下, 程序的格局是一个二进制字符串 它编码了当前迭代中关于程序的所有信息.
我们定义为加上一些分隔符字符, 并定义为一个函数, 当且仅当字符串所编码的格局序列不对应于程序在空输入上的有效停机计算历史时, 该函数将映射到
证明的核心在于证明是上下文无关的. 一旦我们证明了这一点, 我们就能看到在空输入上停机, 当且仅当对所有都有 为了证明这一点, 我们将以一种特殊的方式对列表进行编码, 这种方式使其适合通过上下文无关文法进行判定. 具体来说, 我们将反转所有奇数编号的字符串.
定理 10.5的证明
定理 10.5的证明
我们只概述证明思路. 我们将证明, 如果我们能够计算 那么我们就可以解决 而定理9.4已证明是不可计算的. 令为的一个输入图灵机. 我们将使用图灵机格局的概念, 如定义8.3所定义.
回想一下, 图灵机和输入的格局捕捉了在计算的某个时间点的完整状态. 格局的具体细节并不那么重要, 但你需要记住的是:
- 格局可以由一个二进制字符串编码.
- 在输入上的初始格局是某个固定的字符串.
- 停机格局(halting configuration)会将其某个状态(可以很容易地从中“读出“)的值设置为
- 如果是计算中某一步的格局, 我们用表示下一步的格局.
是一个字符串, 它在除了常数个坐标(那些编码对应读写头位置及相邻两个位置的坐标)外的所有坐标上都与一致. 在这些坐标上, 的值可以通过某个有限函数计算.
我们令字母表 在输入上的计算历史(computation history)是一个字符串 它对应于一个列表(即, 出现在偶数编号块之前, 出现在奇数编号块之前), 其中, 如果是偶数, 则是编码在第次迭代开始时在输入上的格局的字符串;如果是奇数, 则情况相同, 只是字符串被反转了. (也就是说, 对于奇数 编码了在第次迭代开始时在输入上的格局)反转奇数编号块是一种技术技巧, 以确保我们在下面定义的函数是上下文无关的.
我们现在定义如下:
我们将证明以下断言:
断言: 是上下文无关的.
该断言蕴含了定理. 由于在上停机当且仅当存在一个有效的计算历史, 所以是常值1函数当且仅当在上不停机. 特别地, 这使我们能够将判定是否在上停机, 归约为判定对应于的文法是否完全.
我们现在转向该断言的证明. 我们不会展示所有细节, 但要点是: 如果至少满足以下三个条件之一, 则
- 的格式不正确, 即其形式不是
- 包含一个形如的子串, 使得
- 包含一个形如的子串, 使得
由于上下文无关函数在OR运算下封闭, 如果我们能证明可以通过上下文无关文法验证条件1, 2和3, 那么该断言即成立.
对于条件1, 这非常简单: 检查是正确格式可以使用正则表达式完成. 由于正则表达式在否定运算下封闭, 这意味着检查不是这种格式也可以用正则表达式完成, 因此也可以用上下文无关文法完成.
对于条件2和3, 其推理过程与证明“使得当且仅当的函数是上下文无关的“非常相似, 参见练习 10.2. 毕竟, 函数只在其输入的常数个位置上进行修改. 我们将把补充细节的工作作为练习留给读者. 由于当且仅当满足条件1, 2或3之一, 并且所有这三个条件都可以通过上下文无关文法进行测试, 这就完成了该断言以及定理的证明.
正则表达式与上下文无关文法的语义特性总结
总而言之, 我们通常可以在模型的表达能力与分析的易处理性之间进行权衡. 若考虑非图灵完备的计算模型, 则有时能够规避Rice定理, 并回答关于此类模型中程序的某些语义问题. 以下是对我们已探讨的不同模型在语义问题方面已知结论的总结.
| 计算模型 | 停机问题 | 空性 | 等价性 |
|---|---|---|---|
| 正则表达式 | 可计算 | 可计算 | 可计算 |
| 上下文无关文法 | 可计算 | 可计算 | 不可计算 |
| 图灵完备模型 | 不可计算 | 不可计算 | 不可计算 |
- 通用模型下停机问题的不可计算性, 促使我们定义受限的计算模型.
- 在某些受限模型中, 我们可以回答诸如以下语义问题: 给定程序是否会终止? 两个程序是否计算相同的函数?
- 正则表达式是一种受限的计算模型, 常用于描述字符串匹配任务. 我们可以高效地判断表达式是否匹配某个字符串, 并能解决停机问题和等价性问题.
- 上下文无关文法是一种表达能力更强、但仍非图灵完备的计算模型. 其停机问题是可判定的, 但等价性判定不可计算.
习题
考虑以下一种“编程语言“的语法, 其源代码可以使用ASCII字符集书写:
- 变量由字母、数字和下划线序列构成, 但不能以数字开头.
- 语句有两种形式: 一种是
foo = bar;, 其中foo和bar是变量;另一种是IF (foo) BEGIN ... END, 其中...是一个或多个语句的列表, 语句间可以用换行符分隔.
我们语言中的程序就是一个语句序列(语句间可以用换行符或空格分隔).
- 令为这样一个函数: 给定字符串 当且仅当对应于一个有效变量标识符的ASCII编码时输出 证明是正则的.
- 令为这样一个函数: 给定字符串 当且仅当是我们语言中一个有效程序的ASCII编码时输出 证明是上下文无关的. (你无需为指定完整的正式文法, 但需要证明这样的文法存在. )
- 证明不是正则的. 提示见脚注2.
参考书目
与正则表达式的情况类似, 有大量资料对上下文无关文法进行了详细探讨. (Sipser, 1997)的第二章包含许多上下文无关文法示例及其性质. 此外, 还存在如Grammophone这类网站, 允许用户输入文法并观察其生成的字符串及其满足的某些性质.
“上下文无关”这一形容词用于描述CFG, 是因为形式为的规则意味着我们可以始终将替换为字符串 无论出现在何种上下文中. 更一般地, 我们可能需要考虑替换规则依赖于上下文的情况, 这便引出了一般文法(又称“0型文法”, Type 0 grammar)的概念, 它允许形式为的规则, 其中和均为上的字符串. 其思想是, 举例来说, 如果我们希望强制规定仅在两侧均被三个零包围时才应用诸如的规则, 则可通过添加形式的规则来实现(当然, 我们还可添加更一般的条件). 然而, 这种通用性是有代价的——一般文法是图灵完备的, 因此其停机问题是不可判定的. 也就是说, 不存在一种算法 能够对每个一般文法和字符串判定文法是否生成
Chomsky层级是一个文法层级结构, 从限制最弱(能力最强)的0型文法(对应于递归可枚举语言, 参见练习9.10), 到限制最强的3型文法(对应于正则语言). 上下文无关语言对应于2型文法, 而1型文法即上下文有关文法. 这类文法比上下文无关文法能力更强, 但仍弱于图灵机. 具体而言, 对应于上下文有关文法的函数/语言总是可计算的, 且实际上可由线性有界自动机(linear bounded automaton)计算——这是一种占用空间的非确定性算法. 因此, 对应于上下文有关文法的函数/语言类也被称为复杂度类(我们将在第17章讨论空间有界复杂度). 虽然Rice定理意味着我们无法计算0型文法的任何非平凡语义性质, 但对于其他类型的文法, 情况则更为复杂: 某些语义性质可判定, 某些则不可判定, 具体取决于文法在层级中的位置.
1: 如同定义6.3的情况, 我们也可以使用语言而非函数记法, 并说一个语言是 上下文无关 的, 当且仅当满足当且仅当的函数是上下文无关的.
2: 尝试观察是否可以用某种方式在中“嵌入“一个看起来类似于的函数, 从而可以使用类似的证明方法. 当然, 一个函数是非正则的, 并不一定需要使用字面上的括号符号.
