[]
为什么形式化:自然语言与元语言
[] 为什么形式化:自然语言与元语言
2 为什么不用自然语言
日常交流里,自然语言(natural language)足够好用,无论是汉语还是英语。说话的人和听话的人共享大量背景,含糊的地方靠语境补上,补错了再问一句就行。描述程序语言时,这几条都不成立:读规格(specification)的可能是其它国家的编程语言理论爱好者(PLer),可能是十年后的自己,也可能是一台机器。它们当然没法回到10年前问你“你当时**到底什么意思?”。下面几个例子分别展示自然语言在这件事上的一种弊端。
2.1 弊端一:一句话有多种结构
人读到这些句子时会凭常识选一种,常识不同的人就会选不同的那一种。机器没有常识,只能靠写死的规则。
2.2 弊端二:没说到的情形
规格的作者心里多半有答案,只是觉得“显然”而没写下来。问题在于,不同的人眼里显然的东西不一样。C 语言标准就是用英文写的,几十年来,关于某些条款到底允许什么的争论一直没停,后来还出现了专门把它的含义精确化的研究项目(例如 Cerberus)。
2.3 弊端三:“所有都不”?“不是所有”?
讲程序语言的性质时,这类句子到处都是。“每个良类型的程序都不会出错”和“存在一个类型,使得每个程序都有这个类型”,量词顺序一换,意思就完全不同。
2.4 弊端四:自指问题
问题出在“定义”这个词上。短语在定义数,同时又在谈论“什么算定义”,一句话里混了两个层次。自然语言允许这样随意地自我指涉,这极大地提升了自然语言的可表达性和便捷性,代价是有些句子没有任何一致的意思。后文会看到,形式化的做法是把“被谈论的语言”和“用来谈论的语言”严格分开( [定义] 对象语言与元语言 )。
2.5 弊端五:“显然”没法检查
前面四个例子讲的是说清楚有多难。还有一个问题更根本:我们想要的结论是关于所有程序的。“这门语言里,通过类型检查的程序都不会在运行时出错”,这句话谈论的是无穷多个程序。
无穷多个程序没法一个一个试。剩下的办法只有论证,而用自然语言写的论证,读者很难判断它有没有漏掉情形。“其余情形类似”“显然成立”这些话,写的人常常是真心相信的,但这恰恰是错误最爱藏的地方。
2.6 形式化带来了什么
把上面五点反过来,就是形式化要做到的事。
“反过来”的意思是:每一种弊端,都对应自然语言允许了某件事,而形式化的做法是把这件事禁止掉,或者让它必须写明。
- 自然语言允许一串字有多种分组,形式语言就规定每个对象只有一种结构;
- 自然语言允许“没说到”的情形靠常识补上,形式语言就规定没写进规则的情形一律不成立;
- 自然语言允许量词的范围靠语气和语序暗示,形式语言就让范围由符号的位置唯一确定;
- 自然语言允许一句话谈论它自己所在的语言,形式语言就把这两层拆成两门语言;
- 自然语言允许“显然”充当论证的一步,形式语言就要求每一步都写出它用的是哪条规则。
换句话说,形式化不是给自然语言添了什么新能力,而是拿走了它的一部分自由。正是这些被拿走的自由,让写的人和读的人、人和机器,能对同一段文字得到同一个理解。代价也很明显:形式语言啰嗦、死板,写一句“显然”的话可能要好几行。下文会看到,这份啰嗦恰恰是有用的,很多设计上的问题就是在把“显然”展开的时候暴露出来的。
逐条对应如下:
- 结构唯一:语言的语法写成树,一个程序只有一种分组方式(对应 [例] 结构歧义 )。
- 情形穷尽:语言的含义写成有限条规则,规则没有覆盖的情形就是没有定义,不靠“显然”去补(对应 [例] 不完整的规格 )。
- 形状即意思:命题用固定的符号写出,量词的范围由写法决定(对应 [例] 量词的范围 )。
- 层次分开:被研究的语言和研究它的语言是两门语言(对应 [例] 贝里悖论 )。
- 可以检查:证明的每一步都是某条规则的一次应用,别人可以逐步核对,原则上机器也可以(对应上一小节)。
要强调一点:形式化不是把自然语言赶出去。下文的证明仍然用自然语言写,因为人读证明需要自然语言的解释。改变的是“到底在说什么”的最终决定权:自然语言负责讲解,有分歧时以符号写成的定义为准。
接下来要用“树”描述程序的结构,先把相关术语说清楚。
2.7 练习:自然语言弊端
本节练习(5题)
练习1.1:分组和执行顺序是同一件事吗?
一份规格只写了“先乘除,后加减”。它是否同时规定了2 + 3 * 4
2 + 3 * 4
的分组,以及
f() + g()
f() + g()
的调用顺序?设两个函数都会打印一个字符,请给出两份分组相同、打印顺序不同的完整顺序规定。参考答案
“先乘除,后加减”把
2 + 3 * 4
2 + 3 * 4
规定为
2 + (3 * 4)
2 + (3 * 4)
,结果是 ,但没有规定
f() + g()
f() + g()
先调用谁。设
f
f
打印
F
F
后返回 ,
g
g
打印
G
G
后返回 :先左后右打印
FG
FG
,先右后左打印
GF
GF
,两者的数值结果都为 。
可以分别补上“先完整求出左操作数,再求右操作数,最后相加”与相反顺序的规定。分组解决哪个运算包着哪个运算,执行顺序解决哪件事先发生;一条优先级约定不能替代后一条规定。
练习1.2:把量词的范围写出来
检查器接受了甲、乙两个程序,拒绝了丙。“所有提交的程序都没有被接受”有哪两种读法?分别写成不含糊的句子,判断真假,并写出每种读法的否定。提示
区分“每一个都被拒绝”和“至少一个没被接受”,不要只在原句里换一个近义词。参考答案
若把“所有提交的程序都没有被接受”理解为“每个提交的程序都被拒绝”,那么它为假:甲、乙已被接受。若理解为“并非所有提交的程序都被接受”,那么它为真:丙被拒绝。
第一种说法的否定是“至少一个提交的程序被接受”;第二种说法的否定是“每个提交的程序都被接受”。它们的否定也不同,因此原句不能靠读者自行猜范围。这里约定每个提交最终只有“接受”或“拒绝”两种结果。
练习1.3:补全一份除法规格
“a / b
a / b
返回它们的商”漏掉了哪些情况?请自行设计一个整数除法操作,完整规定输入范围和错误处理,并写出三个输入及其预期结果。答案可以有不同设计,但不能把异常情形留空。提示
至少说明可接受的输入、非整除时的结果、除数为零的处理;再给能区分不同规定的例子。参考答案
一种规定是:两边都必须是非负整数,且除数 必须大于 ;返回唯一的非负整数 ,满足 。不满足输入条件时明确报错,不自动把字符串转成整数。
于是
7 / 2
7 / 2
返回 ,
7 / 0
7 / 0
报错,
7 / "2"
7 / "2"
也报错。第一例排除了“保留小数商”的规定,后两例分别排除了“零除返回某个普通数”和“字符串自动转换”的规定。这只是一个可选设计,关键是不能只写“返回商”却把这些决定留给实现者。
练习1.4:谈论一个名字,不等于增加一个名字
一门小语言只允许三个整数名字:“一”“二”“三”,分别表示 、、。那么“这门语言里不能被命名的最小正整数”描述的是哪个数?这句话是否自动成为该语言的第四个名字?说明理由。提示
先检查规定的命名表里有没有那个短语,再问是否偷偷扩充了语言。参照 [例] 贝里悖论 。参考答案
按命名表, 确实是不能被命名的最小正整数,但长短语不在命名表里,所以不是这门语言的名字。我们能在解释规格的自然语言里描述 ,不意味着那门小语言也能命名它。
如果正式把这个短语加进命名表,讨论的就不再是原来那门语言:可命名的数已经改变,原先关于“最小不能被命名的数”的结论必须重新检查。贝里悖论式的混淆,恰恰是把外部描述悄悄算作语言内部的名字,却继续沿用扩充前的判断。
练习1.5:找出“其余显然”的缺口
设 表示关于正整数 的一个判断。某人说:“我验证了 到 ,所以每个正整数都满足 ,其余显然同理。”请给出一个使前一百次验证全部通过、普遍结论却为假的具体 ,并指出这个论证究竟缺了什么。提示
构造一个只对前一百个正整数成立的性质,就能检查有限验证到底证明了什么。参考答案
取 为“”。前一百个正整数都满足它, 却不满足。因此“检查了一百次”只能支持这一百个具体实例,不能独自推出普遍结论。
要证明所有正整数都满足某个性质,还要给出覆盖所有情况的推理方法;“剩下的显然同理”若没有说明相同的前提和推理步骤,只是在重复待证结论,而不是补完证明。
3 两个层次的语言
动手之前,先把 [例] 贝里悖论 留下的教训落实成一条约定。
例如“ 是一个值”是元语言里的一句话,它在谈论对象语言里的 。对象语言自己说不出这句话,它里面根本没有“值”这个词。反过来,元语言里的“所有”“如果……那么”,也不是对象语言的一部分。只要始终分清一句话属于哪一层,贝里悖论那种“一句话同时在两层说话”的情形就不会出现。