跟语言设计相关的问题,往往需要迁就非语言设计者的用户习惯。这可能带来以辞害意的问题——仅仅为了适应习惯,而不是更根本的需求而决定设计。显然这是个不良习气。
更糟糕的是,用户之间也可能因为对习惯偏差不同而产生争议。(在 DSL 中,习惯容易找到缘由上的共识,所以后者主要是通用目的语言设计的问题。)大部分现实情况下,后者可以归咎于设计者欠缺充足的考虑,因为不大可能每个用户的意见都是不合理的,往往从设计者自己没能充分考虑到不同场景的需求开始就埋下了祸根;而被迁就的用户作为帮凶,趋向冲抵人头数来坐实不合理的设计的必要性——剩下的在表达需求话语权中竞争失败用户,通常更倾向于用脚投票而选择其它的设计(即便其它的设计也可能存在类似的不一而足的其它问题)。
考虑到设计者有一贯的历史传统在这类决策中被带到沟里,因此不能等闲视之。另一个问题是,有不少本来简单的普遍问题,在这种氛围中逐渐愚蠢起来了,直至变成了集体无意识的避讳——哪怕稍微思考一下都很容易理解。当然,为了避免浪费时间这样的理由纠结这样的低级问题,倒也无可厚非。诸如缩进用制表符还是空格、大括号后要不要换行的问题……一开始要我这样的沙雕问题的屁股在哪,其实我是拒绝的。因为我觉得……一般人不能跟沙雕一样见识。但是后来沙雕的猴子多了,要控制一下种群数量了,所以就得加特技了。
这样的问题的简单性,首先是词法(lexical) 性质上表现出来的。作为常识,这些被讨论的语言都是应当便于被人阅读的高级语言,所以作为用户需要明白自己预期什么样的视觉(visual) 特性:看得顺眼而便于读写就是好的,否则就是不好的。
不过,要取得共识,判断是否能“顺眼”,不能是纯粹主观的标准,而必须要有客观的依据。对习惯阅读代码的用户,至少在一些基本外观相关的直观效果上,理应是简单的;遗憾的是,事实并非如此。这集中体现在某些用户的双重标准上。一个典型例子:有说法声称类 ALGOL 语言的左大括号({)之后的换行为了避免只有单独大括号的行,以便在屏幕上允许更稠密的代码。然而作为这些语言的用户,稍经过大脑就应当发现这经不住推敲:如果真的是为了避免单独大括号浪费行数,那为什么不使用类 LISP 的风格,不保留单独具有右大括号(})的行,非得只强调左大括号?(即便完全没有类似风格的经验,这一点都不难想象,几乎是个会使用这样的语言的用户都应该能“发明”得出来。)除了经不起推敲的双重标准,更多例子中,部分用户同样地忽略逻辑推理来审视习惯性需求合理性的必要性——结果选择了相对更不合理的设计。例如,使用制表符在文本数据流中表示缩进诚然只是一个变通(制表符自身的确算不上有“缩进”的含义——但这可以通过领域习惯约定补充),但使用本来同样没有“缩进”的含义的固定数量的空格来替代制表符显然在各种意义上(无论是持久存储的不必要开销、编辑器和其它项目配置兼容性相关的编辑和维护体验上)都是更加低劣的;这样,流行的一些观点就更耐人寻味了。(更一般地,在编码、换行、BOM 等文本流相关格式选型的问题上,不少用户也有类似的理解偏差,但这也不只是视觉效果那么直观的问题。)
正常情况下,习惯不是瘾,如果发现明显的不合理,理性的判断应该能够自然地自觉摒弃。这类不够经过大脑的武断判断能够生存甚至借助集体无意识而流行的状况,如果不是成瘾性戒断症状,基本就是跟风的体现。新用户的跟风本身减少了沟通成本,倒不全然是坏事;坏的是,这些只知结论而不知缘由的用户习惯,助长了对审视自身需求的取消主义的反智作风,也普遍地纵容了不良的设计;及至这种现象被习惯地避而不谈,持久地损害了用户合理表达需求的自由。因此,不论是否作为语言设计者的角色的立场上,这都应当打击的对象。
应当强调,这里要求“经过大脑”的首先是分析理由的过程,而不一定需要是具体结果。因为不同领域不同来源的需求不一定存在全然的共识,允许不同本身就是通用目的语言设计需要满足的需求。所以,强迫用户不能使用不同的风格来强行统一,往往是更糟糕的设计,并且更加毫无疑问地容易引起争议。如 Python 这样强制地禁止 ALGOL 传统的允许不同空白符的自由形式(free form) 的来避免风格不统一,就是这样的实例。虽然动机并非完全不合理,在语言中强制这样的设计,并没有解决预期想要避免的问题——而仍然需要如 PEP 8 这样的风格导引来擦屁股。技术上这还有更大的问题:这种强制一致性的设计影响的不仅是视觉效果,还有语义上的缺陷(如检查 IndentationError 对自觉遵守一致性约定的用户的不必要的开销)。整体上看着就是一种 garbage in gargage out 的闹剧——用户自身没能足够反省造成不一致困难的来源,设计者选取了“解决提出问题的人”的思路妄图解决一个原则上就解决不了的问题,结果理所当然地并不能让大多数用户满意。对此视而不见的剩余用户,则和设计者一道充当了鼓动传播反智方法可行性的不光彩角色。
这种强调过程而非结果的方法也可能预期地导致很具体的结论上的不一致。例如,关于 { 不换行是否一定更不合理?实际也不一定。节约行数虽然并非充分的合理理由,但单独 { 会占用屏幕空间而可能影响代码阅读体验是客观的事实——只是是否占用空间的负面影响足够大到应该优先考虑这点需要根据其它需求的相互作用而定罢了。类 ALOGL 语言的用户应容易理解,在 { 后换行的一个直接好处是,配合 } 前也换行,用户在视觉上能够快速定位匹配的 { 和 } 而快速分析代码块的边界。注意这种相对高效的扫描是适应人的视觉特性的二维并行算法,不同于机器的普遍实现。即便有高级的自动匹配括号的编辑器环境使用户能够通常不在乎这样“小”的好处,依赖编辑器也不能在块的定位上取代这里的视觉效率优势(这类似资源预分配不可能取代不进行资源分配的性能优势的逻辑),更不能代替这种策略不依赖文本编辑特性可用性以及不可靠性(如在多种括号错误地没有匹配的情形下)的优势。但在稠密的嵌套代码块中,如果块足够短,那么要求换行的优势可能小到可以忽略,这就是可能值得变通为不换行了——特别是考虑要求对对应的 } 前插入换行的要求可能在复杂的嵌套表达式中(如 C++ 中作为某个 postfix-expression 子表达式的 lambda-expression )表现得很不自然,就不难理解这里的权衡应当优先于词法上的教条结论进行记忆的动机。
理解上述的优势是否足够“小”的问题,在一定意义上无法避免主观理解引入的偏差。实际操作中,这种偏差应当定义为特定领域中特定涉众要求的需求,并在具体代码风格约定规范上继承——而至少排除出语言设计的强制要求之外,以避免毒害语言在不同领域适用的普遍性。指导这种判断可通过对语义的非必要影响来有限地度量,或者至少能一致地偏好顺序以明确其相对效用。具体地,在给定无歧义的需求集的前提下,可定义平凡性(triviality) 来确定“没意义”的下限:若两种风格的选取不能被需求集提供的任意判定方法在接受或者拒绝的结果上体现出差异,则这个风格差异是平凡的,强制其选择结果的规则应当被排除出语言的设计之外。以这个原则考虑,要求消除平凡的语义风格差异(如引入强制缩进)是根本地有害的,因为它混淆了只满足个别涉众的次要的风格差异和满足更普遍涉众需求的、真正更应跟程序自身目的相关的非平凡语义差异(如实现算法引起的程序的差异)。
试图把本应明显适合作为语法差异的处理问题延迟为语义差异解决,可造成在最优化工程成本的意义上的一揽子没事找事的多余问题——不少看上去似乎可能但实际上又无法被可编程地自动有效处理,而不得不要求程序的维护者最终以能从明确需求集无歧义推导确定以外的方式投入精力人工干预(如根据对程序实现的具体目的的理解去判定某个缩进是不是真的漏了),以确保效果上可疑的风格的一致性的本不必要的问题。有一个很直白的诘问:为何不直接要求程序的构造时就使用保证能自动处理的语法性质进行约束(如“把 free form 空白符作为平凡性质而作为不影响语义等价性的性质忽略”的传统策略),来代替这种效果可疑的强制?也许只是设计者单纯没考虑这点罢了,所以造成了这种笑话;但坚持笑话不动摇而持续造成损害,就不是什么笑话了。应当注意,由于已经依赖维护者个别经验的判断依据不可能由语言预设的语法规则决定(最好的、明确无歧义、能在所有维护者之间形成共识的情况下,这也是一个平凡的语义性质),这是一个理论上普遍而无法变通回避的问题。与此相对,若(适合“真正的”)语义差异是非平凡的,则总是需要人工干预,很大程度地回避了效用可疑相关的争议,参见 Rice 定理。当然,也并非所有非平凡语义性质都应该是同等有资格地非平凡的,例如具体同等复杂度的两个不同算法的选取可能是对某个需求平凡的——而应当作为实现细节。这根本上也是工程上应区分作为设计直接内容的接口以及接口的实现的必要性的一个实例;反过来说,这也体现了不合理的语言规则的延迟设计,一个来源是设计者对工程原则上的需求理解的(潜意识的)偏差。
更糟糕的是,用户之间也可能因为对习惯偏差不同而产生争议。(在 DSL 中,习惯容易找到缘由上的共识,所以后者主要是通用目的语言设计的问题。)大部分现实情况下,后者可以归咎于设计者欠缺充足的考虑,因为不大可能每个用户的意见都是不合理的,往往从设计者自己没能充分考虑到不同场景的需求开始就埋下了祸根;而被迁就的用户作为帮凶,趋向冲抵人头数来坐实不合理的设计的必要性——剩下的在表达需求话语权中竞争失败用户,通常更倾向于用脚投票而选择其它的设计(即便其它的设计也可能存在类似的不一而足的其它问题)。
考虑到设计者有一贯的历史传统在这类决策中被带到沟里,因此不能等闲视之。另一个问题是,有不少本来简单的普遍问题,在这种氛围中逐渐愚蠢起来了,直至变成了集体无意识的避讳——哪怕稍微思考一下都很容易理解。当然,为了避免浪费时间这样的理由纠结这样的低级问题,倒也无可厚非。诸如缩进用制表符还是空格、大括号后要不要换行的问题……一开始要我这样的沙雕问题的屁股在哪,其实我是拒绝的。因为我觉得……一般人不能跟沙雕一样见识。但是后来沙雕的猴子多了,要控制一下种群数量了,所以就得加特技了。
这样的问题的简单性,首先是词法(lexical) 性质上表现出来的。作为常识,这些被讨论的语言都是应当便于被人阅读的高级语言,所以作为用户需要明白自己预期什么样的视觉(visual) 特性:看得顺眼而便于读写就是好的,否则就是不好的。
不过,要取得共识,判断是否能“顺眼”,不能是纯粹主观的标准,而必须要有客观的依据。对习惯阅读代码的用户,至少在一些基本外观相关的直观效果上,理应是简单的;遗憾的是,事实并非如此。这集中体现在某些用户的双重标准上。一个典型例子:有说法声称类 ALGOL 语言的左大括号({)之后的换行为了避免只有单独大括号的行,以便在屏幕上允许更稠密的代码。然而作为这些语言的用户,稍经过大脑就应当发现这经不住推敲:如果真的是为了避免单独大括号浪费行数,那为什么不使用类 LISP 的风格,不保留单独具有右大括号(})的行,非得只强调左大括号?(即便完全没有类似风格的经验,这一点都不难想象,几乎是个会使用这样的语言的用户都应该能“发明”得出来。)除了经不起推敲的双重标准,更多例子中,部分用户同样地忽略逻辑推理来审视习惯性需求合理性的必要性——结果选择了相对更不合理的设计。例如,使用制表符在文本数据流中表示缩进诚然只是一个变通(制表符自身的确算不上有“缩进”的含义——但这可以通过领域习惯约定补充),但使用本来同样没有“缩进”的含义的固定数量的空格来替代制表符显然在各种意义上(无论是持久存储的不必要开销、编辑器和其它项目配置兼容性相关的编辑和维护体验上)都是更加低劣的;这样,流行的一些观点就更耐人寻味了。(更一般地,在编码、换行、BOM 等文本流相关格式选型的问题上,不少用户也有类似的理解偏差,但这也不只是视觉效果那么直观的问题。)
正常情况下,习惯不是瘾,如果发现明显的不合理,理性的判断应该能够自然地自觉摒弃。这类不够经过大脑的武断判断能够生存甚至借助集体无意识而流行的状况,如果不是成瘾性戒断症状,基本就是跟风的体现。新用户的跟风本身减少了沟通成本,倒不全然是坏事;坏的是,这些只知结论而不知缘由的用户习惯,助长了对审视自身需求的取消主义的反智作风,也普遍地纵容了不良的设计;及至这种现象被习惯地避而不谈,持久地损害了用户合理表达需求的自由。因此,不论是否作为语言设计者的角色的立场上,这都应当打击的对象。
应当强调,这里要求“经过大脑”的首先是分析理由的过程,而不一定需要是具体结果。因为不同领域不同来源的需求不一定存在全然的共识,允许不同本身就是通用目的语言设计需要满足的需求。所以,强迫用户不能使用不同的风格来强行统一,往往是更糟糕的设计,并且更加毫无疑问地容易引起争议。如 Python 这样强制地禁止 ALGOL 传统的允许不同空白符的自由形式(free form) 的来避免风格不统一,就是这样的实例。虽然动机并非完全不合理,在语言中强制这样的设计,并没有解决预期想要避免的问题——而仍然需要如 PEP 8 这样的风格导引来擦屁股。技术上这还有更大的问题:这种强制一致性的设计影响的不仅是视觉效果,还有语义上的缺陷(如检查 IndentationError 对自觉遵守一致性约定的用户的不必要的开销)。整体上看着就是一种 garbage in gargage out 的闹剧——用户自身没能足够反省造成不一致困难的来源,设计者选取了“解决提出问题的人”的思路妄图解决一个原则上就解决不了的问题,结果理所当然地并不能让大多数用户满意。对此视而不见的剩余用户,则和设计者一道充当了鼓动传播反智方法可行性的不光彩角色。
这种强调过程而非结果的方法也可能预期地导致很具体的结论上的不一致。例如,关于 { 不换行是否一定更不合理?实际也不一定。节约行数虽然并非充分的合理理由,但单独 { 会占用屏幕空间而可能影响代码阅读体验是客观的事实——只是是否占用空间的负面影响足够大到应该优先考虑这点需要根据其它需求的相互作用而定罢了。类 ALOGL 语言的用户应容易理解,在 { 后换行的一个直接好处是,配合 } 前也换行,用户在视觉上能够快速定位匹配的 { 和 } 而快速分析代码块的边界。注意这种相对高效的扫描是适应人的视觉特性的二维并行算法,不同于机器的普遍实现。即便有高级的自动匹配括号的编辑器环境使用户能够通常不在乎这样“小”的好处,依赖编辑器也不能在块的定位上取代这里的视觉效率优势(这类似资源预分配不可能取代不进行资源分配的性能优势的逻辑),更不能代替这种策略不依赖文本编辑特性可用性以及不可靠性(如在多种括号错误地没有匹配的情形下)的优势。但在稠密的嵌套代码块中,如果块足够短,那么要求换行的优势可能小到可以忽略,这就是可能值得变通为不换行了——特别是考虑要求对对应的 } 前插入换行的要求可能在复杂的嵌套表达式中(如 C++ 中作为某个 postfix-expression 子表达式的 lambda-expression )表现得很不自然,就不难理解这里的权衡应当优先于词法上的教条结论进行记忆的动机。
理解上述的优势是否足够“小”的问题,在一定意义上无法避免主观理解引入的偏差。实际操作中,这种偏差应当定义为特定领域中特定涉众要求的需求,并在具体代码风格约定规范上继承——而至少排除出语言设计的强制要求之外,以避免毒害语言在不同领域适用的普遍性。指导这种判断可通过对语义的非必要影响来有限地度量,或者至少能一致地偏好顺序以明确其相对效用。具体地,在给定无歧义的需求集的前提下,可定义平凡性(triviality) 来确定“没意义”的下限:若两种风格的选取不能被需求集提供的任意判定方法在接受或者拒绝的结果上体现出差异,则这个风格差异是平凡的,强制其选择结果的规则应当被排除出语言的设计之外。以这个原则考虑,要求消除平凡的语义风格差异(如引入强制缩进)是根本地有害的,因为它混淆了只满足个别涉众的次要的风格差异和满足更普遍涉众需求的、真正更应跟程序自身目的相关的非平凡语义差异(如实现算法引起的程序的差异)。
试图把本应明显适合作为语法差异的处理问题延迟为语义差异解决,可造成在最优化工程成本的意义上的一揽子没事找事的多余问题——不少看上去似乎可能但实际上又无法被可编程地自动有效处理,而不得不要求程序的维护者最终以能从明确需求集无歧义推导确定以外的方式投入精力人工干预(如根据对程序实现的具体目的的理解去判定某个缩进是不是真的漏了),以确保效果上可疑的风格的一致性的本不必要的问题。有一个很直白的诘问:为何不直接要求程序的构造时就使用保证能自动处理的语法性质进行约束(如“把 free form 空白符作为平凡性质而作为不影响语义等价性的性质忽略”的传统策略),来代替这种效果可疑的强制?也许只是设计者单纯没考虑这点罢了,所以造成了这种笑话;但坚持笑话不动摇而持续造成损害,就不是什么笑话了。应当注意,由于已经依赖维护者个别经验的判断依据不可能由语言预设的语法规则决定(最好的、明确无歧义、能在所有维护者之间形成共识的情况下,这也是一个平凡的语义性质),这是一个理论上普遍而无法变通回避的问题。与此相对,若(适合“真正的”)语义差异是非平凡的,则总是需要人工干预,很大程度地回避了效用可疑相关的争议,参见 Rice 定理。当然,也并非所有非平凡语义性质都应该是同等有资格地非平凡的,例如具体同等复杂度的两个不同算法的选取可能是对某个需求平凡的——而应当作为实现细节。这根本上也是工程上应区分作为设计直接内容的接口以及接口的实现的必要性的一个实例;反过来说,这也体现了不合理的语言规则的延迟设计,一个来源是设计者对工程原则上的需求理解的(潜意识的)偏差。