-
Notifications
You must be signed in to change notification settings - Fork 19
NPL.zh CN
NPL 是 YSLib 提供的语言集合,它在语言规范层次上被设计为可扩展的。
通过派生(derive) 现有的语言( NPL 的方言(dialect) ),避免完全重新设计新的语言,来满足需要一些新语言的场合下的需求。被派生的语言是 NPL 的抽象语言实现。翻译或执行 NPL 或 NPL 抽象语言实现的程序是 NPL (方言)的具体语言实现。具有具体语言实现的方言仍可以派生新的语言作为领域特定语言。
派生领域特定语言的一个其它的例子是 XML 。
NPL 的语法基于形式文法上可递归构造的表达式。
在操作语义(基于项重写系统)的意义上,其中的子表达式又称为项。
非正式地,NPL 使用类似 S-表达式的基本语法,但不使用二元有序对(和终止符号)而直接支持列表;即表达式直接以是否为括号作为边界,分为列表表达式和非列表表达式。
正式语法中,作为子表达式的项可以是列表或非列表项的复合表达式规约列表项中的一部分代替有序对的地位,以要求任意项可被无歧义地进行从左到右的语法分析。
NPL 只要求小括号作为列表表达式的边界。其它替代的括号由派生实现约定。对于适用于多个 NPL 实现的可移植代码,避免使用其它语言中习惯作为代替括号边界的字符表示替代的括号以外的含义,特别地,[ISO C++] 文法 balanced-token
中的边界字符 ()[]{}
。
NPL 对标识符的限制较为宽松。[ISO C] 和 [ISO C++] 的所有标识符都是 NPL 标识符。但派生实现可加以限制。
NPL 不提供专用的注释语法。以特定形式的项(如表示字符串的字面量)替代注释是预期的惯用法(idiom) 。这不妨碍派生语言可能添加预处理器扩展特性。
NPL 提供了一些通用的概念和公共规则,但不构成具体语言实现的完整语义规则。语义规则由派生实现补充完整。
注释
排除注释及中缀标点 ;
和 ,
,NPL 的语法和 Scheme 语言或Kernel 语言的语法近似。不过,NPL 不支持构造循环引用,也不提供相关语法。
设计满足的需求描述参见这里(en-US) 。
需求来源:
- 出发点:构建一个可用计算机实现的语言。
- 基本目的:在以标准 C++ 环境( [ISO C++] 定义的宿主实现(hosted implementation) )的程序框架中嵌入配置和脚本操作。
- 扩展目的:渐进地向独立的计算机系统演进,探究能适用于各个领域并以计算机实现的通用目的语言(general-purpose language) 。
本文档描述基于此出发点的 NPL(Name Protocoling Language) (一个替代的递归缩写是“NPL's not a Programming Language”,因其不仅适合作为 PL 的元语言特性及其参照实现。
和大部分其它设计不同,为了确保一定程度的适应通用目的的性质,它们被设计整体首要考虑。这样的设计的语言是(自设计(by desing) 用于)满足通用目的的语言(general-purposed language) 。
NPL 是独立设计的语言,但它和 [RnRK] 定义的 Kernel 语言有许多核心设计的相似之处,尽管设计的一些基本特征(如资源可用性基本约定)以及基本哲学相当不同。
NPL 的主要实现的核心部分实质上支持了 Kernel 的形式模型—— vau 演算(vau calculi) 。
注释 另见操作语义。
具体的 NPL 语言在这些模型的基础上提供。
NPL 的命名即体现了 vau 演算和传统 λ 演算为模型的语言的核心差异:
强调允许在对象语言中指定求值上下文的显式求值(explicit evaluation)(而非 Lisp 方言中以 quote
为代表的显式干预默认的隐式求值)的风格以及表达式求值前后的不同,特别地,关注在语言中直接表达的名称和求值后指称的实体的不同。
更进一步地,NPL 普遍地支持区分一等引用和被引用的一等实体并具有更精确的资源控制机制,这是与 Kernel 的主要设计上的差异。
关于 vau 演算的形式模型和其它相关内容,详见 [Shu10] 。特别地,vau 演算提供了 fexpr 类似的抽象。
注释 另见求值算法设计的实例。
关于一些其它支持 fexpr 特性的语言设计,参见:
和 Kernel 以及本设计不同,这两个例子的设计使用动态作用域;在主要的特性中存在一些关键的不同而在形式模型的适用性上有显著的区别。
注释
NPL 和历史上同名的 John Darlington 的 NPL (New Programming Language) 没有直接渊源;特别地,后者的多个等式的函数定义语法和高阶类型没有被内建支持,而静态类型和纯函数式限制被避免。
Kernel 语言的原始参照实现是 SINK 依赖 MzScheme version 103 的解释器实现,和 [RnRK] 有一定差异。例如,字面量 #ignore
和 #inert
用 %ignore
和 %inert
代替。
klisp 是 Kernel 语言的一个更完善的实现。
有些特性(如复数支持)都没有在这两个中实现提供,而仅在 [RnRK] 中指定。
在 YFramework/NPL 提供一些参考具体语言实现。当前 YFramework 主要使用抽象语言实现 NPLA 的具体派生(derived) 的实现 NPLA1 ,在这个基础上用于不同的目的,如程序配置、动态加载的 GUI 等。
NPLA 提供了比大多数现有的程序设计语言更强大的一般抽象。这集中体现在:
这意味着 NPLA 是本质上动态的语言,但和一般语言不同,用户可以很大程度上动态地替换现有语言实现,包括在运行时替换一个解释实现为一个或多个优化编译器。这也意味着语言设计上既不需要区分解释实现和编译实现(本质上不对立),也不需要区分动态和静态(因为随时能从基础语言上构造出静态子集)。
这样的特性设计在绝大多数语言中不存在并且几乎无法支持。已知唯一的例外是 Kernel ,在这些特性上有极大的相似,尽管实际上基本特性是独立设计的,并且在基本设计哲学上有极大不同( NPL 和 [RnRK] 中明确的 guidelines有很大不同且基本不兼容)。不过,考察设计的完整性,NPL 的派生语言也从中借鉴了一些重要的设计:
一些值得注意的和类似语言的主要设计差异(原理详见开发文档):
- NPL 和 Kernel 类似,强调一等对象,但含义有所不同。此处的“对象”和 [ISO C] 及 [ISO C++] 中的定义类似,具有比 Kernel 更严格的含义。
- NPLA 和派生实现的语法和整体的求值类似 Scheme 和 Kernel 大多数基于 S-表达式的 Lisp 方言,但有一些显著的区别。
- 和 Scheme 不同,而和 Kernel 一致,NPLA 避免顶层(top-level) 和局部的上下文的差异。
- NPLA1 明确区分约定包括列表项的求值规则。和传统习惯不同,NPLA1 中括号明确不需要表示应用的含义,这可以减少一些场合(如命令行)需要输入的过多的连续括号。
- 和 [RnRS] 定义的 Scheme 以及 [RnRK] 定义的 Kernel 一致,不支持某些 Lisp 方言的方括号
[]
替代圆括号()
的语法。 - 不提供注释语法。
- 语言实现中内置预处理处理中缀
;
和,
,作为前缀合并子$sequence
和list
的语法糖。两者的含义和 Kernel 中的相同(类似 Scheme 的begin
和list
)。
- 和 Kernel 相似而和 Scheme 不同,使用操作子及一等环境和
eval
代替 Scheme 的卫生宏(hygienic macro)(en-US) 及宏展开的作用。- 和 Kernel 类似,鼓励使用直接求值风格而不是引用(quote) 。
- 不过 NPLA 也提供了
$quote
的派生而非如 Kernel 一样完全避免。
- 和 Kernel 不同,NPL 明确支持资源抽象,不保证支持循环引用,而 NPLA 明确不支持循环引用。
- NPLA 明确支持基于 [ISO C++] 实现的对象模型和互操作,且明确不要求支持全局 GC 。
- 从在互操作的目的出发,和 C++ 具有相似性和相容性。
- 在 vau 演算的论文 ([Shu10]) 中,提及不支持全局 GC 有较大的管理开销(admistrative cost) 但没有详细讨论和语言特性的联系。
- 即便不支持全局 GC ,当前实现仍然明确支持 PTC(proper tail call) 。
- PTC 基于语言规则而不是实现行为定义,详见 proper tail recursion ,这里和 Kernel 提供的保证含义一致。
- 没有在其它语言发现这种不支持全局 GC 和支持类似 C++ 副作用的情形下的 PTC 支持的先例。
- 和 Kernel 不同,NPLA 不完全强制对象类型的封装;且基于支持互操作的考虑,支持开放的类型系统,而不要求覆盖所有值(即要求对象类型分区(partition) )。
- 对机器数(不论是整数还是浮点数)的操作被剥离了,当前不被支持,需要用户代码添加个别操作。
- NPLA 的规约框架和 vau 演算的操作语义几乎完全一致,不过实际上(因为先前语言设计上的不确定)显著地保留了更多的可扩展和可修改性。
当前派生实现的 NPLA1 由 YFramework 提供 API 。其中包括 REPL (read-eval-print loop) 的解释实现。外部文件的形式的 NPLA1 脚本可被基于这些 API 实现的 stage 1 SHBuild 调用并用于 YFramework 的构建。
由 YFramework 对外部编码的假设,NPLA1 实现加载的文件流的剩余内容的编码视为 UTF-8 ;同时支持 CR+LF 或 LF 为换行符。
注释 这些实现基于 YSLib API 提供互操作支持。
仅在此给出本文档中的外部引用的名称。其它引用文献的内容详见 YSLib 项目文档 doc/NPL.txt
。
- [ISO C] ISO/IEC 9899
- [ISO C++] ISO/IEC 14882
- [ISO C++11] ISO/IEC 14882:2011
- [ISO C++14] ISO/IEC 14882:2014
- [ISO C++17] ISO/IEC 14882:2017
- [ISO C++20] ISO/IEC 14882:2020
- [WG21] (ISO/IEC JTC1/SC22/WG21) C++ Standards Committee Papers
- [WG21 P0135R1] Richard Smith, Wording for guaranteed copy elision through simplified value categories, 2016-06-20.
- [RnRK] Revised Report on the Kernel Programming Language
- [Shu09] John N. Shutt, Revised-1 Report on the Kernel Programming Language, Technical report WPI-CS-TR-05-07, Worcester Polytechnic Institute, Worcester, MA, March 2005, amended 29 October 2009.
- 注释 当前 [RnRK] 只有 n = -1 的版本。引用确切版本时,同 [Shu10] 中的用法,使用 [Shu09] 标记;但引用时一般同样不涉及其版本差异。
- [Shu10] John N. Shutt, Fexprs as the basis of Lisp function application; or,
$vau
: the ultimate abstraction, Ph.D. Dissertation, WPI CS Department, 2010. - [RnRS] Revised Report on the Algorithmic Language Scheme
- [Fl91] Matthias Felleisen, On the Expressive Power of Programming Languages, Science of Computer Programming Volume 17, Issues 1–3, December 1991, pp. 35–75.
- [EGAL] James Noble, Andrew P. Black, Kim B. Bruce, Michael Homer and Mark S. Miller, The Left Hand of Equals, Onward! 2016: Proceedings of the 2016 ACM International Symposium on New Ideas, New Paradigms, and Reflections on Programming and Software, October 2016, pp. 224–237.
- [So90] Harald Søndergaard and Peter Sestoft, Referential transparency, definiteness and unfoldability, Acta Informatica 27, 1990, pp. 505–517.
- [Rust] The Rust Reference
- 不定期更新。
- [Fi94] Andrzej Filinski, Representing Monads, POPL '94: Proceedings of the 21st ACM SIGPLAN-SIGACT symposium on Principles of programming languages, February 1994, pp. 446–457.
- [Hi90] Robert Hieb, R. Kent Dybvig and Carl Bruggema, Representing Control in the Presence of First-Class Continuation, ACM SIGPLAN Notices, Volume 25, Issue 6, Jun. 1990, pp. 66–77.
- [Racket] Racket Documentation
- 不定期更新。
- [Chu41] Alonzo Church, The Calculi of Lambda-Conversion, Annals of Mathematics Studies, Princeton: Princeton University Press, 1941.
- [Bare84] Hendrik Pieter Barendregt, The Lambda Calculus: Its Syntax and Semantics [Studies in Logic and the Foundations of Mathematics 103], Revised Edition, Amsterdam: North Holland, 1984.
- [Cl98] William D. Clinger, Proper Tail Recursion and Space Efficiency
- [IEC 60559] ISO/IEC 60559
- [ECMAScript] ECMA-262
- [ECMAScript 2019] ECMA-262 10.0
本节描述被本文档中的一些原理讨论引用的的公共依据。
原则指关于设计和实现的哲学,同时作为一般规则约束设计和实现的工程阶段。
关于需求特别是通用目的语言的讨论,参见需求概要(en-US) 。
为使论述有效,约定本体论(ontology) 规则。
基本的本体论规则是约束逻辑系统构造的公理。
有效的陈述(如需求描述)应保证操作上可预期结果。
在此意义下,缺乏约束性的规则不可预期的风险是代价。
推论:规则应适当约定适用范围,以避免外延不清。
语义的存在体现本质。
仅仅应用语法规则,即限定为语法的文法(syntactic grammar) 的形式系统归纳的设计,不被视为表示任何有效的含义。
名义概念的内涵和外延应被足够显式指定,避免指涉上的歧义,允许构造有效的陈述。
不可分的同一性(the identity of indiscernibles) (en-US) 比较陈述的客体之间是否相同而不需要被重复地处理。
价值观是关于价值判断的规则,其输出为二元的值,决定是否接受决策。
作为应对普遍需求场景的不同解决方案选型时的价值判断的抽象归纳,价值观被作为比较是否采用设计相关决策的全局依据。
以下陈述形式表达价值优先的选项,同时作为公理。
注释 相同推理结果仍然可能不唯一,这来自于自然语言描述的输入的不精确性。
在明确需求的前提下,尽可能保证对现状按需进行改变的可行性和便利性。
适用于一般需求。
对计算机软件或其它可编程的实体:尽可能避免不必要地损失可修改性,便于保障按需引入或除去接口及其实现的自由。
原理
一般地,需求可能随着不可控的外部条件变化。假设已明确的需求不变只能适合相当有限的情形。积极应对变化能提供价值。
尽可能消除对满足需求无意义的代价,减少影响需求实现的整体成本。
适用于一般需求中设计决策的比较。
对计算机软件或其它可编程的实体:不为不需要的特性付出代价。
注释
一个类似的表述:
Efficiency has been a major design goal for C++ from the beginning, also the principle of “zero overhead” for any feature that is not used in a program. It has been a guiding principle from the earliest days of C++ that “you don’t pay for what you don’t use”.
在满足需求的前提下,尽可能使用符合倾向减小实现需求代价的单一良基关系下具有极小元的接口设计。
注释 减小实现需求的代价,如减小设计工作量。
这是一条模式规则,依赖具体情形何者符合良基关系的极小元这条非模式规则作为输入。
实际使用时,非模式规则可以直接指定为二元关系的子集,或者一种良序的度量。
注释 例如,“公开函数声明数”“模块数”。
这个输入也可能直接对应符合需求集合的某种最小功能集合而不需要附加度量,如表示某种设计的裁剪。
注意规则指定的基数是对实现需求有意义的代价,因此不涵盖避免不必要付出的代价。
在确定的范围内尽可能少地提供必须的接口,避免不必要的假设影响接口适应需求的能力,同时减少实现需求的各个阶段的复杂性。
适用于一般需求的实现,特别地,强调“通用”目的时。
对需要在计算机上实现的人工语言设计:设计语言不应该进行功能的堆砌,而应该尽可能减少弱点和限制,使剩下的功能显得必要。
Programming languages should be designed not by piling feature on top of feature, but by removing the weaknesses and restrictions that make additional features appear necessary.
— [RnRS] & [RnRK]
注释
其它各个领域中的实质等价一些表述包括:
- 用于安全系统设计的最小特权原则。
- 用于自然科学理论设计的奥卡姆剃刀(Occam’s Razor) 原理,避免不必要的假设引入诉诸无知(argument from ignorance) 谬误。
关注点分离(separation of concerns, SoC) 原则 :局部设计的内容应和需求的陈述或其它隐含的设计前提分别一一对应。
适用于一般需求的实现,特别是其中依赖认识论观点的过程。
原理
这条规则利用需求和设计内容陈述中概念外延普遍蕴含的局域性(locality) ,提供给定代价下更多的可行性或求解给定问题时使用较小的代价,用于:
- 应对不可控复杂条件下使问题可解。
- 局部可复用现有解的子集。
此外,尽管并非总是必要,应用知识内容的简单假设、最小接口原则和本原则可在认识论上导出还原论。
根据作为需求的价值观,归纳适用于通用目的语言应有的构成及其性质(形而上学(metaphysics) )的设计规则,包括三条价值判断实现公理:
具备这些性质的设计可视为由价值判断蕴含,预设前提为真的设计方法论的实现。
注释
注意和 worse is better 或 the MIT approach 不同,设计的性质并非完全并列。特别地,完整性和一致性都被作为正确性的一部分考虑。
因为变化的自由,具体需求以及判断正确性和简单性的确切依据都可能会随着项目的进展而变化。
设计应正确地反映需求,不在需求的基础上新增作为实现细节以外的不确定性。
无法确保满足这种正确性(correctness) 要求时,不应继续设计。
正确性包含可行性(feasibility) 。
若无法满足正确性,则需求输入存在问题。
正确性不包含但应逻辑蕴含设计的一些其它性质。若无法实现,则具体性质的定义存在问题。
保持正确性作为设计评价的首要依据以使决策简单,同时能符合价值判断。
正确性应蕴含完整性(completeness) ,即确保没有需求被遗漏。
推论:设计应包含完整的需求响应。
原理
对通用编程语言的一个完整性要求是支持计算上的可表达性(expresiveness) 。
这种性质被称为可有效计算性(effective computability) ,或 Turing 完备性(Turing completeness) 。在可物理实现的计算普遍遵循 Church–Turing 论题(Church–Turing thesis) 的情形下,这同时是可计算性(computability) 。以上性质一般不加分辨。
具体的语言中允许的表达的可计算性是表达能力(expressive power) 。另见 [Fl91] 。
特定的场合要求更弱的性质。例如,类型检查等情形需要全(total) 计算而确保实现总是可终止。这种要求在完整的实现中可通过附加的设施(用户提供的标注或证明)保证,而不应通过系统设计的规则静态地排除,否则实现是不完整的。仅在作为领域特定语言时,通过从需求中排除可计算性,静态规则作为优化是被允许的。
正确性应蕴含一致性,即内部的逻辑无矛盾性。
推论:设计应保证一致性。
在满足正确性的前提下,接口设计应尽可能满足简单性(simplicity),即尽可能少地具有可被继续简化的内容。
接口设计的简单性优先于实现的简单性。
可修改性(modifiablity) :在满足需求的前提下,修改应尽可能少地有碍于其它的接口。
这是变化的自由的推论。
泄漏的抽象(leaky abstraction) 指抽象的底层复杂性没有被抽象合理地隐藏,而在一定程度上构成了利用抽象时的不必要的依赖。
这种抽象泄漏(abstraction leak) 的结果直接和避免不必要付出的代价、关注点分离原则和简单性冲突。
同时,抽象的有效性被削弱,泄漏构成的不被预期的依赖难以满足正确性;只要有避免抽象泄漏的方法,就不满足最小接口原则。
因此,只要可能,避免抽象泄漏。
注释 在信息安全意义上,抽象泄漏还可能提供难以抵御的附加的攻击信道。
为了可实现性,宿主(host) 系统对总的资源(典型地,运行程序需要的存储)有未指定的上限。
除此之外,接口抽象不附加接口语义要求以外的限制。
这个原则同时利于满足正确性和简单性。而不遵循这个原则的设计在接口描述上违反最小接口原则。
在允许实现的前提下,附加具体特性上的使用限制(如 [ISO C] )可放宽对实现的要求;但无原则地随意选取此处的限制不足以直接证明具体的限制的有效性,而依赖实际实现的情况才能判断,造成抽象泄漏。
注释 实例:PicoLisp 使用符合此原则的设计。
开放性(openness) :除非另行指定,不假定实体不存在。
这个原则主要用于建模(modeling) 的依据。对一般的模型,这个原则称为开放世界假定(open-world assumption)。
与之相对,封闭世界假定(closed-world assumption) 需要提前设置一个全集(universe) 以保持至少在逻辑的意义上合规。
开放世界的元素的全集是模型的结构化规则推断得到的,而非名义上的定义决定。这同时称为模型的语言的论域(universe of disclosure) 。
原理
封闭世界假定表面上可能简化实现,但在一般的模型中是不必要的,因为保持问题合规性的论域应已由清晰的需求描述规范,不应为此阻碍实现变化的自由。
使用封闭世界假定的一个主要实用意义是使模型在有限的信息下能推理出逻辑上更强的结论。在重视结论的知识系统中,这通常是一种优化;但在重视表达能力(而通过其它方式辅助求解问题)的通用模型中,这种前提是一种直接的限制。同时,封闭世界假定的优化不保证对所有输入有效,对否定输入还可能导出一些矛盾。
注释
开放世界包含的元素的外延及其语言的论域伴随随语言规则的修改而改变。
开放世界不限制论域中的某个子集是封闭的。例如,论域中可能存在某个子集的所有元素通过一定方式被枚举。
语言设计独立于语言实现。
这种分离允许避免抽象泄露。
典型地,使用提供接口抽象层作为必要构造的架构方法,即分层设计。
最小特权原则(principle of least privilege, PoLA) :除非有必要,接口抽象不提供满足需求以外的其它信息和资源。
这是最小接口原则在限制适用领域前提下的等价表述之一,用于避免不必要的访问路径引入额外的安全(safety) 风险,更容易满足(针对恶意使用风险的)安全性(security) 和可信性保证相关的需求。
实质上提供例外的必要性之一是接口正确性:不附加不存在于需求以外的安全设计;根据可修改性,这应是实现细节。
最小依赖原则(principle of least dependencies) :除非有必要,接口实现仅使用必要的依赖。
这是最小接口原则的推论之一,其非模式规则的输入为:
- 已知必要的依赖较已知必要的依赖和不必要的依赖的并集要求较小的使用和维护成本。
- 这里的使用包括演绎抽象自身的推理(reasoning) 。依赖较少时,推理时需要搜索的解空间也越小。
依赖倒置原则(dependence inversion principle) 在单一模块下包含以下含义:
- 抽象(的接口)不应该依赖(实现)细节。
- (实现)细节应依赖抽象(的接口)。
这是最小依赖原则应用在不同抽象的模块化设计中使用以下公设的推论:
抽象是细节包含的子集,依赖抽象的接口较依赖实现细节具有更少的依赖。
设计应具有可复用性(reusability) :高层抽象设计的实现应包括复用此设计的实现的设计。
这是最小接口原则的推论之一,其非模式规则的输入为以下公设:
一般地,高层抽象设计和复用此设计的实现较单一的高层设计的实现更复杂。
此前提条件由对需求工作量可行性分析中的度量验证总是成立。
推论:除非必要,不分离抽象设计的实现和复用此设计的实现的设计,避免复杂性。
全局意义上的不分离设计不违反关注点分离原则。
注释
典型实例:语言是一种高层抽象设计,语言的库(library) 是一种复用语言的设计。因此,语言实现应包括库设计。
组合(composition) 是一种特定形式的涉及多个实体的复用,允许复用时不修改被复用的其它实体。
可组合(composability) 原则:接口的设计应允许不同设计之间的组合满足这些设计响应以外的需求。
这是最小接口原则的推论之一,其非模式规则的输入为以下过程推断得到的引理。
公设:一般地,在存在充足基础解决方案的情形下,组合现有解决方案的设计较重新给出不依赖这些解决方案的设计的解节约成本。
应用避免不必要付出的代价,得到引理:
一般地,在存在充足基础解决方案和满足需求限制的情形下,组合现有解决方案的设计优于重新设计。
即提升可组合性可减少实现被复用的设计的成本。
接口的设计应具有统一性(uniformity) :尽可能避免特例。
无限制的特例要求指定更多的附加规则避免潜在的违反一致性的风险,而违反这个要求。
因为不需要特设只有对象语言中可用的规则,复用元语言规则有利于实现统一性。
以统一的方式复用元语言和对象语言公共设施在语法设计上称为光滑性(smoothness) ,而这可推广到语义上(另见正交性),以避免对抽象能力(power of abstraction) 的限制([Shu10] §1.1.2) 。
原理
在语言设计上,这类似 [RnRK] 的设计原则 G1 :
以上原则在 NPL 中略有变化。
同 [RnRK] ,被 G1b 重现能力的特性是内建的(built-in) 。这不同于如 [RnRK] G2 指定的基本的(primitive) 特性。
[RnRK] 的基本特性指不要求作为派生的(derived) ,即以对象语言程序实现的特性。而内建特性适合整个语言规范的接口设计约定,不论其实现是否被派生。不被要求重现的部分是实现细节。
但是,因为基本特性不要求能通过对象语言特性的组合实现,在不考虑派生特性的可实现性时,G1b 不会限定基本特性的能力。
整体上的 G1b 在和正确性冲突时不被要求。这也避免了 [RnRK] §0.1.1 指出的“妥协”。
因为语言规范不依赖使用对象语言表达,G1b 仅表示用户使用语言的扩展,不表示语言自身的可扩展性;后者通过满足需求的能力和强调支持开放性体现。
仅通过用户程序实现的这种原则在 NPL 的设计中不被视为必要。但偏离这个原则的设计一般同样是不必要的。
注释
关于 G1a 的改变,详见一等实体和一等对象。
设计应提供适用性(usability) :合乎预期满足的问题领域的特性。
对通用目的的领域,应进行权衡。
注释
这个原则存在以下的侧重不同使用方式或场景的具体表述。
结合用户的经验,这个规则的变体是之一最小惊奇原则,强调降低接口的学习和适应成本。
设计应符合易预测性(predictability) :允许但难以偶然实现的危险操作。
同 [RnRK] 的设计原则 G3 。
这里的危险的操作指引起较大代价的不预期或无法预期结果的操作。
这是变化的自由和避免不必要付出的代价的推论,包含两方面:
- 避免危险操作的风险是正规性和避免不必要付出的代价的推论。
- 不直接禁止危险的操作以满足上述的允许变化的要求。
避免危险的操作在许多上下文中可减少程序中易错(error-prone) 的实现的风险。
一旦提供特性,应提供可用性(availablity) :保证一定程度的典型场景下能被使用。
绝大多数情形都不能使用的特性是对接口设计的一种浪费,很难符合也通常不符合简单性。
可用性的概念有时也指抽象和实体具有的符合这个原则的属性。
最小惊奇原则(principle of least astonishment):在保持合理性的前提下,若能评估目标用户的接受能力,避免违反其直觉的设计。
其中,合理性至少应蕴含正确性,一般也蕴含简单性和适用性同时不违反其它原则(特别应注意尽量保持可复用性和可组合性)。
这个原则主要适用于人机交互接口的设计,但也适用于一般的 API 。
推论:约定优于配置(convention over configuration) :约定接口的合理的默认行为,而不是隐藏其行为而提供配置另行实现。
在满足正确性的前提下,接口的设计应具有正交性(orthogonality) :根据需求适当分解为排除冗余和重复且能合理组合的部分。
一般地,正交的设计使相同目的可使用更精简的接口组合方式实现。这也使接口具有更强的抽象能力。
方法论(methodology) 是严格独立价值判断的规则,是关于价值判断结果参数化的判断规则。
不同的价值判断的结果作为方法论输入,决定是否适用此方法。
其它方法详见以下各节。
注释
一些规则因其主要表述包含价值判断而不在此归纳为方法论,尽管其中一些表述中的前提可以被参数化(如奥卡姆剃刀的“如无必要”的具体必要条件)。
Premature optimization is the root of all evil (or at least most of it) in programming.
— The Art of Computer Programming
原始含义适合计算机程序设计中以效率为目标的决策。
扩展的外延适用于一般需求,要求:
- 适时收缩理论长度以照顾可操作性。
- 注意断言一个优化过早自身可能就是一个过早的优化。
- 主动适应需求变更。
- 不同时明确全部的具体需求,只限定需求范围:能使用计算机实现部分语义的任务。
封装(encapsulation) 是接口设计的合理性准则。
封装是不可分的同一性的一种实现方式:封装提供的接口以下的所有实现在接口从使用者的角度都是不可分的。
注释 若存在使用者可感知的抽象泄漏,这种实现可能失效。
以接口的预设风格的价值判断为输入,封装性要求接口满足以下多态性(polymorhism) :
给定接口的替代接口,则替代接口应能代替原接口,当且仅当不引起非预期的可观察的差异。
在语言设计中,去除风格参数化的这条原则被作为 LSP(Liskov Substitution Principle) 。
参数化风格限定并非任意符合 LSP 的接口设计都符合封装性要求。这便于从不期望的设计中剔除不符合其它原则的设计。
注释
一些程序设计语言中的封装提供符合 LSP 的面向对象风格的设施。这些设施把数据和代码组合在一起提供,但仅仅组合并不体现封装性。因此,同时具有信息隐藏的特性,如 [ISO C++] 的类成员的访问控制的机制,被认为是典型的封装。
即便如此,封装在严格意义上和信息隐藏是相互独立的。即便语言不提供信息隐藏而仅仅指定违反封装性不关心实现细节的假设的操作未定义,也不失去封装性。事实上,[ISO C++] 中,使用 reinterpre_cast
无视类的访问控制就是这种例子。
另一方面,LSP 事实上关于子类型,不限于以类作为类型的基于类的面向对象风格,实际外延更广。
信息隐藏(information hiding) 保持不需要公开的信息不被公开,以使设计符合最小接口原则并支持避免抽象泄漏。
适用于接口及其实现。
信息隐藏以是否需要公开信息的价值判断(特别地,关于如何符合最小接口原则)的结果参数化。
注释
封装的接口通常有助于实现信息隐藏。直接限定避免接口规格具有过多的信息,是另一种直接的实现方式。
例如,基于类的面向对象通过对名称组成的表达式限制对类成员的外部访问,隐藏了类成员的信息,同时提供封装性。
其它方式可直接不在外部提供任何访问被封装实体的名称,如 [RnRK] 的封装类型(encapsulate type) 和 [ECMAScript] 通过 WeakMap
实现的封装。这些封装也同时实现了被封装实体的信息隐藏。
即便如此,如关于封装的讨论指出的,封装不一定需要实现信息隐藏。更一般地,信息隐藏的目的也不一定是提供封装。例如,系统的安全性可能直接在需求上要求隐藏特定信息,不论这种信息是否关于某种接口的实现。
接口和实现的设计应具有足够的模块化(modularity) :被划分为若干保持联系的组件即模块(module) ,至少满足正确性和可组合性,并强调实现可复用性。
模块化设计通常有利于使设计具有正交性,但模块化相对可复用性,更侧重可组合性。
参数化的输入是需被评估模块化程度的结构设计(包括模块的粒度(granularity) 和组成部分的依赖关系)相对给定需求的实现质量的价值判断。
从对正确性的强调可知,较简单性优先考虑通用性(generality) 。
这和 [RnRK] 中讨论的设计哲学虽然相当不同,但仍允许和 Kernel 具有相似的特性。
作为典型的 NPL 的一个派生实现,NPLA1 具有以下和 Kernel 相似的核心设计:
- 相似的求值算法(差异详见 NPLA1 求值算法)。
- 环境可作为一等对象。
- 支持 vau 抽象,且使用词法作用域。
- 强调支持对象语言中的显式求值风格及表达式求值前后的不同。
- 强调直接求值而非传统 LISP 方言的
quote
。
在附录之前的以下章节给出 NPL 的正式规格说明的公共部分,即语言规范。
本文档仅提供部分派生实现的规格说明。关于其它具体规格说明,详见 YSLib 项目文档 doc/NPL.txt
。
在不和其它语言规则冲突时,派生实现可能补充或覆盖更确切范围中生效的定义和具体语言规则。
仅在不致混淆时使用。
- 实现(implementation) :语言实现。
- 环境(environment) :实现环境。
- 外部环境:(当前描述的系统边界的)外部的实现环境。
- 派生实现(derived implementation) :派生语言实现。
以下术语的定义参见计算机体系结构。
- 指令
- 指令集
- ISA
注释
具体讨论设计策略另见需求描述文档。
另见设计原则的讨论;对本章内容的描述的理解应符合其中的原则。
可用计算机实现的语言首先是计算的模型(model of computation) ,或者计算模型,对计算进行建模得到。
与之相关地,为计算机系统建模作为计算机的模型(model of computer) ,需对有限计算资源的现实进行适应。
被计算机实现的语言应同时具有这两方面的特征。
作为实用的语言,语言还应强调提供可编程性以允许用户利用;这样的语言称为编程语言(programming language) 。
本设计尝试在语言的原生设计中应对现有语言缺乏模型问题以避免这些妥协带来的消极影响,同时取得比非模型方法更强的可用性。
这种可用性至少体现在语义的精确性可通过模型直接决定;仅为精确性,不需要另行补充模型设计(尽管现有模型可能仍然是不完全形式化的)。
原理
以无限的计算资源为前提,理想的模型无法被物理地完全实现,无法直接作为计算机实现的语言的模型。
同时,这些模型仅适合对计算建模,并没有强调允许可编程性的实现;扩充可编程设计而保持模型自身的主要性质相当困难。
因此,基于计算的模型适配编程语言的设计必然需要妥协:对这些模型的裁剪和补充能提供若干编程语言的模型,但这无可避免地显著地复杂化模型自身,且不利用用户使用简单有效的规则实现通用目的上的可编程性。
事实上,使用严格形式化的模型描述编程语言的行为较编程语言自身的发展更落后:
- 大部分编程语言并没有使用模型支持它们的设计。
- 现实的实用语言,特别地,包括所有主流的工业语言(industrial language) ,几乎都没有在语言规范中给出完整的模型。
- 通常的实用语言只形式化基本的语法上的规则,无法指导用户精确理解程序的含义。
这些落后集中体现在的语义模型的缺失,使对编程语言语义的判断取决于规格说明中模型外规则的理解。
后验(postpone) 的语义模型可以使用不同形式语义方法设计,但和语言规范差异的一些本应避免的附加工作,并且通常难以完整地作为标准规格的描述。
注释
Turing 机、无类型 λ 演算(untyped lambda calculus) 等早期计算模型不考虑有限计算资源限制。
特定的算法过程具有计算复杂度要求。除非另行指定:
- 这些复杂度是任意避免符合错误条件的方式调用时求值蕴含的渐进(asymptotic) 时间复杂度。
- 若指定边界,明确的输入规模以哑变量(dummy) n 表示。
- 指定复杂度的计算保证可终止(terminate) 。
注释
算法过程也适用对象语言上的操作。
在抽象机的配置中,任意通过元语言(metalanguage) 语法描述的资源总是可用的。
为避免对具体资源的总量和实现细节做出假设,除此之外,本设计只要求模型蕴含所有权语义(即便不严格形式化——注意作为元语言的描述模型使用的形式语言仍然可能是实现细节)。
具体计算机系统的实现中,保证基本可用的资源被直接映射到程序执行(execution) 的环境中。尽管和适配的软件环境相关,这最终由硬件实现物理地保证。
原理
因此,有必要做出基本的可用性约定以允许表达明确的要求以避免不可实现。
为尽可能解决模型相关的问题,优先以通用目的而不是领域特定(domain-specific) 语言作为评估语言特性设计的参考原则。
领域特定语言的特性应能合理地从支持通用目的的特性中派生,且不影响实际的可用性。
形式语义方法主要有公理语义(axiomatic semantics) 、指称语义(denotational semantic) 和操作语义(operational semantics) 。
操作语义可分为在模型中指定具体规约步骤状态的结构化操作语义(structural operational semantics)(或小步(small-step) 语义),及仅指定规约的输入和输出的自然语义(natural semantics)(或大步(big-step) 语义)。
注释 抽象机和演算是使用操作语义的模型的两类例子,虽然后者也可以对对象语言以外的表示建模而实现其它的语义方法。
非确定语义:经验语义,不需要使用自然语言解释的部分。
本文档不直接给出形式语义。语言规则确定的经验语义可在一定条件下转写为上述形式语义方法表达的形式。
程序是语言的具体派生。实现程序即在语言的基础上指定具体派生规则。
语言实现外的程序是用户程序(user program) 。
以程序或另行指定的其它形式实现的可复用程序被归类为库。
注释 一般地,不论是语言实现还是用户程序,都可能使用库。
除非另行指定,一个程序支持多个库的实例,之间不共享内部的状态。
语言特性包含不依赖库的核心语言特性(core language feature) 和库特性(library feature) 。
NPL 是抽象的语言,没有具体语言实现,但一些直接影响实现表现形式的规则被本节限定。
NPL 的实现可进行抽象解释(abstraction interpret) ,其目标不一定是程序。
任一 NPL 实现(和派生实现)的符合性由以下 NPL 符合性规则定义:文档指定的满足对实现的要求的语言规则子集,包括本节、基本文法、语义和其它派生实现定义的规则。
这类规则总是包含对应语言的语义的 NPL 公共子集,且蕴含实现行为的要求。
语言规则约定的未指定的程序或实现的属性及实现行为在符合性要求上等价。满足这类规则的前提下,实现选取特定的未指定的属性及对未指定行为的特定实现的选择不影响实现的符合性。
原理
基于抽象机可直接定义最小的符合性要求,如 C++ 的规则。
NPL 没有直接在此定义同等具体的规则,而以一般的要求取代。这允许派生实现对不同的具体规则进行补充和调整。特别地,这允许不同的方式提供语义规则。
蕴含实现行为的要求的一个主要例子是关于状态的规则。除了允许由实现定义和派生实现指定的不同,这实质上提供和上述具体规则等价的默认情形,而简化派生实现需要的对语言规则的补充和调整。
一个 NPL 的完整实现应保证行为能符合以下的执行阶段(phase of execution) :
- 分析(analysis) 阶段:处理代码,取得适当的 IR 。
-
(目标)代码生成(target code generation) :以 IR 作为输入,生成可被其它阶段执行的代码,即目标代码(target code) 。
- 注释 一般意义的代码生成可以有多个子阶段,包括多种内部 IR 的翻译,直至得到最终目标代码(final target code) 作为输出。
- 运行:运行生成的最终目标代码。
- 注释 最终目标代码的形式视不同而定,可能有附加的封装格式。例如编译器的目标代码(object code) 经链接(linking) 为可执行的映像(image) ,被加载后形式才能运行。
其中分析阶段是任意实现必要的,依次包含:
- 词法分析(lexical analysis) :必要时转换字符编码;转义(escape) 并提取记号。
- 语法分析(syntactic analysis) :语法检查(检验语法正确性)并尝试匹配记号和语法规则中的语法元素。
- 语义分析(semantic analysis) :语义检查(检验语义正确性)并实现其它语义规则。
以上的具体阶段不要求和实际实现中的一一对应,但应保证顺序一致。
运行之前的阶段总称为翻译(translation) ,包含各个翻译阶段(phase of translation) 。
对有宿主语言支持的嵌入实现或目标不是程序的情况,代码生成及之后的阶段不是必须的。
宿主语言实现可提供作为客户语言的 NPL 的本机(native) 实现。
宿主语言实现提供 NPL 实现环境,同时对 NPL 环境的操作可影响 NPL 程序,这些情形都是元编程,NPL 在此同时是对象语言。
嵌入实现的宿主语言可直接运行语义分析的结果(中间表示)。
在语义不变的前提下,允许实现一次或多次翻译部分代码产生部分中间结果并复用。
运行时(runtime) 程序实现运行阶段。
其它可能的阶段由派生实现定义,但应满足所有阶段具有确定的全序关系,且不改变上述指定的阶段的顺序。符合这些条件的附加阶段称为扩展阶段。
注释
字符编码是被翻译的源中的二进制表示相关的模式。
一个实现可能具有计算模型意义上的并发属性,即并发实现(concurrent implementation) 。
一个实现中顺序执行以上执行阶段的一组状态称为一个执行线程(thread of execution) ,简称线程(thread) 。
一个实现在整个执行过程中可以有一个或多个线程被执行。是否支持多线程执行(多线程翻译和/或多线程运行)由派生实现定义。
若实现支持多线程执行,则执行阶段的状态区分不同的并发执行线程,此时具体的状态构成由实现定义。
若某些状态在某个执行阶段 k 被唯一确定为不可变状态,且在之后的状态下是不变量(invariant) ,则此状态称为满足 k 阶段不变量约束的。
正确性(correctness) 规则约束被执行的程序,包含语法正确性和语义正确性。
当正确性规则被发现违反时,实现进入异常执行状态。
翻译时正确性规则以外的异常执行条件和状态由派生实现定义。
翻译时的异常状态要求给出用于区分正常状态特定的行为作为诊断,包括诊断消息和其它派生实现定义的实现行为。
语法正确性规则是翻译时正确性规则。
部分形式上的正确性规则在翻译时确保。
允许翻译时确保的形式上正确的程序是合式的(well-formed) ;反之不合式(ill-formed) 。
合式的程序符合语法和语义的正确性的规则。
其中,实现被要求确保通过翻译的程序符合语法规则和翻译时确保的可诊断(diagnosable) 语义规则。
不合式的程序不保证被完整地翻译,应在运行前终止执行阶段。
错误(error) 是不满足预期的正确性或其它派生实现定义的不变性质时的特定诊断。
非正确性或不满足这些不变性的条件是错误条件(error condition) 。
满足错误条件时,实现可引起(signal) 错误。
注释
和 [RnRS] 中的某些版本指定错误可以不诊断不同,引起错误蕴含诊断。
实现的行为由具有存在非特定空间上限的存储的抽象机(abstract machine) 描述。这种描述对应的语言的语义是抽象机语义(abstract machine semantics) 。
若语言规则明确特定的行为可被忽略,则被忽略之后的实现行为与之前在语言规则中视为等价。翻译的实现可选取这些等价行为中的任一具体行为。
不论程序是否满足正确性规则,实现对程序的执行都可能存在未定义行为,此时实现的行为不需要满足正确性规则指定的行为要求。
特定的语言规则引入未定义行为。程序的执行在适用这些规则指定的条件时,引起未定义行为。
特定的语言规则排除未定义行为的引入,以满足一定的可用性。这不排除程序的执行可能因同时使用的其它语言规则引起的未定义行为。
注释
抽象机语义是一种操作语义。
抽象机语义也可非形式地定义语言的正式的(normative) 语义和行为要求,例如 C++ 抽象机 。
一个派生实现使用外部语言 L 简单实现模型 NPL-EMA ,若满足:
宿主语言提供的实现环境称为宿主实现环境,简称宿主环境(host environment) 。
注释
若支持多线程执行,需要附加的显式同步。
这种实现可能提供宿主多线程对应的实体,其中包含需要的被隔离的资源。
其它语言的实现也可能提供类似的设计,例如 V8 的 v8::Isolate
。
本章约定基本的 NPL 文法规则中,包括语法及对应的基础词法。对应的语义在下文列出。
多态文法规则:派生实现可完全不提供本章明确定义的词法和语法构造的支持,仅当提供同构的替代文法且符合语义规则。
本章定义的对象语言的正式文法以 BNF(Backus–Naur Form ,Backus–Naur 形式)作为元语法形式地描述,且在与对象语言之间无歧义时不在终结符(terminal) 边界使用引号(即 "
和 '
)。
注释
因为这种 BNF 使用符合 highlight.js
支持的语法,在 Markdown 源代码中直接使用支持的语言别名 bnf
标记。
- 字符(character) :组成语言代码的最小实体。
- 基本翻译单元(basic transation unit) :作为翻译输入的任意连续字符的有限序列(可以是空序列)。
- 翻译单元(translation unit) :基本翻译单元的集合,之间满足由派生实现定义的规则。
程序以翻译单元或具体操作指定的以翻译单元进行翻译得到的其它变换形式表示。
- 字符集(character set) :对一个实现而言不变的字符的有限集合。
- 基本字符集(basic character set) :实现环境必须支持的字符集。具体由派生实现定义。
- 字符串(character string) :字符集上的序列。
除非另行指定,关于字符集定义的其它概念同 [ISO C++11] 对 character 和 character set 的有关定义。
注释
字符编码基于字符集定义。
一般地,一个翻译单元只具有同一个字符编码。
词法规则(lexical rules) 约定在字符基础上的最小一级的可组合为语法元素单位直接关联的文法规则。
约定元语言语法 <x>
表示词法元素 x
,::=
表示定义,|
表示析取。
文法:
<token> ::= <literal> | <$punctuator> | <$identifier>
- 分隔符(delimiter) :代码中标记特定字符序列模式的字符序列。
- 词素(lexeme) :代码中以分隔符确定边界的字符序列。
- 记号(token) :词素的顶级分类。
属于记号的语法元素可以是以下的词法分类:
- 字面量(literal) :一种记号,参见以下描述。
- 标点(punctuator) :由派生实现定义的特定字符序列的集合,可具有一定语义功能。
- 标识符(identifier) :除字面量和标点以外的记号
代码中邻接的分隔符和非分隔符不构成一个词素。
不在记号内包含的空白符是分隔符,而不是词素。
标点是分隔符,也是词素。
超过一个字符的标点可能在匹配字符序列确定是否构成词素时具有词法歧义。此时,应指定消歧义规则确保存在唯一可接受的匹配方式,或引起词法错误终止翻译。
除非派生实现指定,字面量以外的记号不包含分隔符。
记号是可能附带附加词法分析信息的词素。词法分析后得到的记号可以用词素映射到词法分类的有序对表示,但 NPL 不要求在此阶段保持分类也不限定表示的构造。
可以保证 [ISO C++11] 的 identifier 的定义,或在上述标识符中插入字符 $
构造得到的标识符属于 NPL 标识符。
派生实现可定义其它能构成标识符的词素。
注释
NPL 不指定超过一个字符的分隔符,因此默认没有词法歧义。派生实现可指定这些规则。
NPL 是自由形式(free form) 的语言,空白符原则上不构成字面量以外的词素和语义。
文法:
<char-escape-content-seq> ::= <$single-escape-char> | <$escape-prefix-char><$escape-content-seq>
<char-seq> ::= <$literal-char> | <char-escape-seq>
包含 <char-escape-seq>
的 <char-seq>
包括:
\'
\"
\\
\a
\b
\f
\n
\r
\t
\v
<char-seq>
的含义同 [ISO C++] 的对应转义序列。
注释 这是 [ISO C++] 的 <simple-escape-sequence>
词法分类中除了 "\?"
的情形,也是 [R6RS] 在 <string element>
中支持的字面情形。
文法:
<literal-content> ::= <char-seq> | <literal-char-seq><literal-data>
<code-literal> ::= '<literal-content>'
<data-literal> ::= "<literal-content>"
<string-literal> ::= <code-literal> | <data-literal>
<literal> ::= <string-literal> | <$derived-impldef-literal>
- 代码字面量(code literal) :以 ' 作为起始和结束字符的记号。
- 数据字面量(data literal) :以 " 作为起始和结束字符的记号。
- 字符串字面量(string literal) :代码字面量或数据字面量。
- 扩展字面量(extended literal) :由派生实现定义的非代码字面量或数据字面量的记号。
- 字面量(literal) :代码字面量、数据字面量、字符串字面量或扩展字面量。
派生实现定义的解释可排除代码字面量作为字符串字面量。
原理
传统的字面量一般是自求值项,这包括一般的字符串字面量。
代码字面量可提供非自求值项的处理方式。
以下单字符标点是 NPL 图形分隔符:
(
)
,
;
以下单字符标点是 NPL 分隔符:
- NPL 图形分隔符
- 空白符(字符串 " \n\r\t\v" 中的字符之一)
注释
空白符同 [ISO C++] std::isspace
在 C 区域下的定义,不含空字符(null character)
。
原理
NPL 图形分隔符可不和其它字符组合而作为单独的记号。因此,这不包含构成字面量的字符 '
和字符 "
。
NPL 分隔符用于一般分隔记号(而不是识别字面量)的外部描述,也没有显式地包含这些字符,但词法分析仍应把按字面量规则把这些字符作为必要时区分不同记号的边界。
以下规则(按优先顺序)定义了词法分析转换输入为输出的步骤:
- 反斜杠转义:连续两个反斜杠被替换为一个反斜杠。
- 引号转义:反斜杠之后紧接单引号或双引号时,反斜杠会被删除。
- 断行连接:反斜杠之后紧接换行符的双字符序列视为续行符,被删除使分隔的行组成逻辑行。
- 字面量:未被转义的单引号或双引号后进入字面量解析状态,无视以下规则,直接逐字节输出原始输入,直至遇到对应的另一个引号。
- 窄字符空白符替换:单字节空格、水平/垂直制表符、换行符被替换为单一空格;回车符会被忽略。
- 原始输出:其它字符序列逐字节输出。
不对空字符特殊处理。
注释
因为不一定是 NPL 分隔符,转义字符不总是分隔标识符。
本节指定 NPL 作为对象语言的语法。
约定元语言语法 <x>
表示语法元素 x
,::=
表示定义,|
表示析取。
程序被作为语言实现组成部分的语法分析程序规约,结果能确定其和一定的语法元素匹配。
规约时应进行语法规则的检查。
NPL 的基本语法单元是可递归构造的表达式,或派生实现指定的其它语法构造。
构成基本语法单元的规则参见词法规则。
文法:
<expression> ::= <atom-expression> | <composite-expression> | <list-expression>
表达式(expression) 是受表达式语法约束的记号序列,可以是:
构成表达式的表达式是被构成的表达式的子表达式(subexpression) 。
文法:
<atom-expression> ::= <token>
原子表达式不能被表示为其它表达式的语法构成形式的复合。
文法:
<composite-expression> ::= <token-expression> | <expression-token>
复合表达式是原子表达式和表达式的复合,即语法意义上的直接并置连接(juxtaposition) ,不在被复合的表达式之间存在其它记号。
同一个表达式可能被按原子表达式出现的位置以不同的方式规约为复合表达式。允许的规约复合表达式的方式由派生实现定义。
文法:
<separators> ::= <separator> | <separator><separators>
<expressions> ::= | <expression> | <expression><separators><expressions>
<list-expression> ::= <left-list-bound><expressions><right-list-bound>
<left-list-bound> ::= ( | <extended-left-list-bound>
<right-list-bound> ::= ) | <extended-right-list-bound>
其中,<separator>
是 NPL 分隔符。
列表表达式是在其他表达式的序列(可能为空)左右附加一组 <left-list-bound>
和 <right-list-bound>
作为边界构成的表达式。
<left-list-bound>
和 <right-list-bound>
是不同的标点。
边界为 (
和 )
的表达式是基本列表表达式。其它可能的边界由派生实现定义,构成扩展列表表达式。
注释
对 <list-expression>
的定义,以形式语义使用的元语言 中的元语法符号 *
扩展元语法,可以简记作 <list-expression> ::= <left-list-bound> <expression>* <right-list-bound>
。
NPL 的名称(name) 是符合语法规则约束的若干记号的集合。
存在非空的名称集合可被作为表达式。
原理
语言规则对语言可表达的名称添加要求,以使语言的源代码能够直接使用名称。
名称在源代码形式之外也可广泛存在,且能通过不唯一的方式构造。因此,语言规则允许不和源代码形式一一对应的名称。
注释
构成名称的集合的表现形式不唯一。
特定的名称可能为空集。
约束通常包含顺序,即其中的记号构成确定顺序的序列。
记号或记号集合经编码,一般可实现为可表达的字符串。
语法形式(syntactic form) 是词法上满足特定形式的语法构造。
除非派生实现另行指定,语法形式总是表达式。
以派生实现定义的标点结尾的表达式称为语句(statement) 。
语句语法的分组(grouping) 规则以及是否隐式地作为列表表达式求值由派生实现定义。
一个派生实现使用简单文法 NPL-GA ,若满足:
原理
NPL-GA 允许一些典型的分析器(parser) 简化设计作为实现。
在表达式的形式文法仅作为语法规则,使用词法分析的结果提供作为语法类别(syntactic category) 的词素的串作为输入的情况下,NPL-GA 支持 LL(1) 文法分析,即使用 NPL-GA 语法。
若延迟复合表达式和列表表达式中的选择到分析器外(之后可能由语义处理),检查语法的判定程序可进一步简化,仅判断记号 (
和 )
的匹配。
若词法分析处理直接对 (
和 )
和进行记号化(tokenize) 标记,则 NPL-GA 分析器不需要支持其它判定。这样的分析器实现的 NPL-GA 子集等效 LL(0) 文法。但由于 NPL-GA 不限定语法元素具体数量,等效 LL(0) 分析器当且仅当输入的串终止时接受输入,因此是平凡的(trivial) ,通常不具有实际意义,因为:
- 形式上这里只有算法步骤的多少的差异,而几乎所有实现的语言都不把它作为可观察行为。
- 即便需要统计串的长度,也应可以在之前(词法分析)计算,使用语法分析完成这个任务在此是低效的。
反之,在分析 NPL-GA 语法前扩展其它语法预处理(preprocessing) 规则可以支持更多的文法扩展。这样的文法扩展可接受扩展的非 NPL-GA 文法,但仍允许保持语法分析器的实现使用 NPL-GA 语法。
NPL 的语义规则构成演绎系统(deductive system) (en-US) ,通过对翻译单元中的表达式的求值表达。
除非派生实现另行指定,仅使用表达式指定关于对象语言中的计算的语义。
基本语义规则要求:
NPL 允许程序具有语义等价的未指定行为。派生实现可能通过约定和限制其具体选项的选取以指定更具体的实现行为。
本章内的语言语义基于一种EBNF(扩展 BNF ) 的方言,其中:
- 规则定义符号(defining symbol) 为
::=
。 - 除非元素的含义和用法被单独另行指定,作为文法类的元素使用
<
和>
作为分隔符。- 注释 这和 BNF 相同,但和常见 EBNF 可能不同。
- 在和对象语言之间没有歧义的情形时:
- 使用
*
作为修饰前一个元素的 Kleene 星号,表示之前修饰的项可出现 0 次或多次。 - 使用
(
和)
组合之间的元素。
- 使用
- 若对象语言直接出现元语言的元符号(metasymbol) 而出现歧义,需要用一对
"
或'
标记。- 注释 此时和非原始版本的 BNF 以及常见 EBNF 可能相同。
原理
ISO/IEC 14977:1996正式定义了最常见的方言,但这一标准没有简化业已分歧的元语法方言,以至于仅在 ISO 的不同编程语言标准中,就存在多种记法并用。
BNF 作为元语法形式地描述适用描述形式语言的串的重写系统,而非对描述语义更灵活的项重写系统。
在语法之外,元素之间的空白符通常不是重要的。因此,可直接省略元语言分隔符 <
和 >
。这也是大多数 EBNF 的典型用法(即便描述的是语法)。描述语义的例子如 [Shu10] ,其中使用特定的字体支持元语言中出现的元素。
然而,为便于在代码中表示而不依赖特设的字体,此处仍然使用 <
和 >
,除非元素的含义和用法被单独另行指定。
注释
因为这种 EBNF 使用不符合 highlight.js
支持的语法,在 Markdown 源代码中不使用支持的语言别名 ebnf
标记,而记作自定义语言标记 xbnf
。
ISO EBNF 被 highlight.js
的 ebnf
语法定义支持。
[Shu10] 也使用 ::=
作为规则定义符号。
- 区域(region) :和特定位置代码关联的有限实体集合。
- 范围(range) :一个连续区间。
- 此处“连续”的概念由派生实现定义,默认参照数学的形式定义。
- 声明(declaration) :引入单一名称的表达式。
- 声明区域(declarative region) :对某一个声明及其引入的名称,通过声明区域规则决定的范围。
- 有效名称(valid name) :可以唯一确定指称的实体的名称。
- 有效命名实体(valid named entity) :有效名称指称的实体。
- 名称隐藏(name hiding) :若同一个名称在同一个位置属于超过一个声明区域,则应能通过名称隐藏规则确定唯一有效的声明以指定有效名称和对应的有效命名实体,此时有效名称隐藏其它声明区域声明的名称,有效命名实体隐藏可以使用被隐藏名称指称的实体。
- 作用域(scope) :声明区域的子集,满足其中指定的名称是有效名称。
- 生存期(lifetime) :逻辑上关于实体的可用性的连续区间的抽象,是一个闭集。
- 属性(property) :实体表现的性质。
- 同一性(identity) :实体上的一种等价关系,允许实体具有标识不相等特定的属性。
- 注释 特定属性的例子如占据存储。
- 对象(object) :可确定同一性的实体。
- 值(value) :表达式关联的不可变状态。 * 作为实体,对象总是关联值作为它的内容,称为对象的值(value of object) 。
- 未指定值(unspecified value) :未指定的值。
- 修改(modification) :使状态被改变的操作。
- 作用(effect) :语言支持的一定上下文内的表达式规约蕴含的计算作用。
- 副作用(side effect) :对表达式的值以外的表示的改变的作用。
- 幂等性(idempotence) :重复后即不改变状态的性质。
- 项(term) :特定的演绎系统中处理的对象,是带有基本递归构造的元素,可对应语法中的表达式。
- 注释 这样的演绎系统主要是项重写系统。
- 子项(subterm) :具有递归形式构造的文法描述的参与构成项的项。
- 变量(variable) :通过声明显式引入或通过演绎系统规则隐式引入的以名称指称的实体。
- 绑定(binding) :引入变量的操作或结果,其中后者是变量的名称和引入的被变量表示的实体构成的有序对。
- 约束变量(bound variable) :子项中出现的名称被绑定的变量,即其指称可能依赖具体上下文的变量。
- 同名的约束变量的整体重命名替换不保证不改变指称进而可能影响语义。
- 自由变量(free variable) :子项中出现的非约束变量。
- 组合子(combinator) :不是变量也不含相对任何项的自由变量作为子项的项。
- 常量(constant) :满足某种不变量的约束以和不可变状态关联的实体。具体由派生实现定义。
- 注释 不和变量对立:蕴含不可变状态的变量可能是常量。
- 转换(conversion) :根据基于特定等价性(假设)前提的两个项之间的自反的演绎。
- 规约(reduction) :两个项之间的、实例是某个转换的子集的满足反自反的演绎。
- 抽象求值(abstract evaluation) :对表达式的不取得作用的规约。
- 具体求值(concrete evaluation) :对表达式的取得作用的规约。
- 求值(evaluation) :抽象求值或具体求值。
- 注释 即对表达式的规约。
- 求值结果(evaluation result) :作用的子集,是求值得到的用于替换被求值的表达式作为它的值的实体,或其它由派生实现定义的实体。
- 不和其它结果混淆时,简称结果(result) 。
- 求值中取得求值结果中的表达式的值的过程称为值计算(value computation) 。
- 值计算包含确定用于替换的实体以及替换的过程,两者之间具有因果性。
- 值对象(value object) :表示值的对象。
- 注释 值对象是可作为值使用的对象,例如作为求值结果的一部分。和值不同,值对象不一定是不可变状态。
- 控制状态(control state) :实现中决定求值的状态。
- 程序表现的控制状态通称控制(control) 。
- 特定控制状态的改变使不同的实体被求值,这对应控制转移(transfer) 。
-
调度(schedule) 特定可能改变控制作用的实体可决定如何转移控制状态。
- 注释 这可能实现并发的计算。
- 除非派生实现另行指定,控制状态是区分多线程执行中不同线程的状态。
- 控制作用(control effect) :引起控制状态改变的作用。
- 在 NPL 中,控制作用是在对象或派生实现定义的实体上引起改变的副作用。
- 相等关系(equality relationship) :定义在值的集合上的等价关系。
- 布尔值(boolean value):逻辑真或逻辑假。
- 谓词(predicate) :若具有结果,则结果是布尔值的实体。
- 数据结构(data structure) :数据的构造性表示。
- 一等实体(first-class entity) :语言表达的允许支持足够特性的子集的实体,其中特性支持包括:
- 可作为语言中的有效命名实体。
- 可作为语言中的值在特定的对象语言构造中使用。
- 可表达数据结构。
- 满足以上支持的值域中没有任意特设的限制。
- 注释 使用的判定准则和 [RnRK] Appendix B 的 first-class object 的约定实质上一致。
- 一等对象(first-class object) :可确定同一性的一等实体。
- 访问(access) :从实体上取得状态或修改实体。
原理
一些设计中,值对象是专用于表示(不可变的)值的对象(en-US) 。本设计不使用这个定义,因为:
- 值对象作为对象,蕴含表示的目的,在语言设计而非实现的上下文中不是值的等义词。
- 以实现角度考察值对象提供值的表示时,不关心它是否可作为一等对象而要求不可变可允许其上的副作用替换表示具有其它的值。
- 作为对象的值,它可能因为互操作等目的在外部被直接作为其它语言实现中可作为(允许可变的)一等对象的实体。
注释
在实现执行的上下文,生存期概念兼容 ISO/IEC 2382 的 lifetime 定义:
portion of the execution duration during which a language construct exists
定义绑定的有序对作为抽象表示,不需要被对象语言支持。对象语言可支持其它具体的有序对数据结构。
典型地,作用包括计算得到的值、引起的副作用以及其它可由区域和变化的状态二元组描述的实体。
一等对象同时是对象。
为满足可在表达式中通过求值被使用,一等实体总是能关联表达求值结果的值,称为实体的值。
表示用于表现演绎实例、具体实现及其中一部分实体的状态。
注释 其中的一部分实体可以是某个值。
因为保证同一性,对象的值作为可变状态的表示时,即对象存储的值。
注释 变量不一定是可变状态的表示。
外部表示和内部表示是相对的。不同外部环境可以有不同的外部表示,这些外部表示相对其它外部环境而言可以不是外部表示。
外部表示可能被读取(read) 处理为内部表示。内部表示可能被写入(write) 处理为外部表示。
读取和写入操作的副作用分别是输入(input) 和输出(output) 。
外部表示为元素序列时,读取和写入是非特定格式数据和元素序列之间的转换,若不含其它作用,其操作是进行反序列化(deserialize) 和序列化(serialize) 。
内部表示为对象时,读取和写入包含对象和非特定格式数据之间的转换,其操作是进行列集(marshall) 和散集(unmarshall) 。
除非另行指定,不要求对象语言提供内部表示到外部表示的转换。
文法约定基准的表示作为翻译的输入。这种表示是翻译所在外部环境的外部表示,即源代码;翻译结果是对象语言代码,简称对象代码(object code) ,可以是另外的外部表示。
翻译单元是这里被翻译的外部表示。
由基本文法,空白符和参与的表示,不一一对应。为便于输出标准化,NPL 约定以下规范(canonical) 外部表示:
- 对列表,输出的表示是以
(
和)
作为边界,元素以单个 - 对非列表的存在唯一的对应词法形式(如字面量)的值,输出这个值的词法形式。
- 其它值的外部表示未指定。
谓词在模型中表示为数学关系、映射或单值函数;在对象语言中可有不同的表示,如函数。
其它外部表示和内部表示的外延由派生实现定义。
外部表示和内部表示可能部分共享相同的规则。这些表示是同像的(homoiconic) 。语言支持同像的表示及其有关特性的性质是同像性(homoiconicity) 。
典型地,同像性允许复用代码和数据之间的表示。特别地,同像性允许对象语言中的代码作为数据(code as data) ,而不需要显式地处理为和代码不同的数据结构,显著简化元编程的接口复杂性。
除非另行指定,NPL 和派生实现不限制语言中任何不同表示之间可能具有的同像性。
原理
存储程序的体系结构自然而普遍地依赖代码和数据具有相同的表示,以便有效地把存储的数据直接作为代码提供给控制部件。
存储程序型计算机因此能自然地支持自修改代码。在更高层次的抽象中,高级语言可能改变这些性质的可用性,使其符合最小接口原则,符合安全设计的需要。
但是,自修改程序在一些情形下仍然必要。为了通用目的,这些设计应符合易预测性,而非完全禁止。
通过语言规则指定的同像性不具有体系结构设计时依赖的具体数据表示容易引起非预期操作的风险,有必要作为公开特性。
不需对代码和数据分别提供不同的特性有利于语言设计和使用的简单性。
这同时使对象语言不需要提供特设的对自身的反射(reflection) 特性,因为潜在可被反射的对象伴随一般的元编程无处不在而可被随时引入或排除,直至另行指定的规则限制这种能力。
这也是使对象语言的设计符合光滑性的主要机制。
注释
自修改程序在一般意义下对运行时生成代码的 JIT(just-in-time) 编译器 是必要的。这有助于提升程序运行时性能。
另行指定的规则包含显式的涉及表示的转换规则,例如语法分析等阶段可能转换外部表示为不同的(不确定种类的)内部表示,这些表示不保证其中的组成在变换前后一一对应。但是,NPL 规则没有明确指定破坏可能具有对应的规则,因此不同内部表示之间的非同像性仅在派生实现中可能指定。
演绎系统具有的演绎规则决定演绎推理(deductive reasoning) 的输出。
指定转换输入和输出之间的关系的演绎规则是转换规则。
两两可转换的对象的传递闭包构成等价类,称为可转换等价类。除非另行指定,以下只讨论具有单一可转换等价类的转换规则演绎系统,即(抽象)重写系统(rewriting system) 。
对象之间的转换保持某种等价关系的等价变换(transformation) 。对象之间的规约是其中的子集,即以存在等价关系的一个对象替代另一个对象的有向转换。
若两个对象具有规约到相同结果的变换,这两个对象可连接的(joinable) 。
若任意两个对象等价蕴含对象可连接,则此重写系统具有 Church–Rosser 属性(Church–Rosser property) 。
若可从任意一个对象规约到的任意两个对象可连接,则重写系统具有汇聚性(confluence) 。
若可从任意一个对象的一步规约到的任意两个对象可连接,则重写系统具有局部汇聚性(local confluence) ,或称为弱汇聚性(weak confluence) 。
若可从一个对象规约到的任意两个对象可连接,则此对象具有汇聚性。
若可从一个对象的一步规约到的任意两个对象可连接,则此对象具有局部汇聚性,或称为弱汇聚性。
规约中可包括涉及实现环境的交互。
若规约用于求值,汇聚性限定为:满足任意以此规则变换前和变换后的项被分别规约时,两者的作用相等。
作为重写系统的实例,一个项重写系统(term rewriting system) 包含以下组成:
- 语法元素(syntactic element) 的集合。
- 项及其子项是语法元素的非空的串。
- 辅助语义函数(auxiliary semantic function) 的集合。
- 可通过语义变量(semantic variable) 指称其中的元素。
- 重写规则(rewrite rule) 的集合。
语法上蕴含自由变量的项是开项(open term) 。闭项(closed term) 是开项在项上的补集。
为表达计算,限制特定的重写关系使之不满足自反性,得到规约关系(reduction relation) ,即指定规约。对应地,双向的重写规则限制为其子集的单向的规约规则(reduction rule) 。经限制的系统是项规约系统(term reduction system) 。
规约关系视为表达计算查询(computational query) 的项和答案(answer) 的项之间的映射。此时,项规约系统被作为一种计算模型。
- 注释 为表达计算的答案的确定性,需要确保规约可能取得范式。
一般地,项规约系统关联的结构总称为演算(calculus) 。
对每个演算,存在和项对应的上下文(context) 。元语言中,一般的上下文以语义变量 C
表示,形式化为具有元变量(meta variable) □ 的以下构造:
C ::= □ | ...
其中 ...
是演算支持的项的语法中替换子项得到的对应构造。
注释 例如,对作为对象语言的 λ 演算,语义变量 x
表示约束变量,其上下文为:`C ::= □ | (CT) | (TC) | (λx.C) 。这里的记法基本同 [Shu10] 。
一般的项记作语义变量 T
,则 C[T]
表示上下文 C
中作为元变量通过语法代换(syntactic replacement) 为项 T
的结果,它是一个项。
作为对象语言的变量的项可依赖不同的上下文指称不同的实体。
一个变量 x
被上下文 C
捕获(capture) ,若对任意 x
是其自由变量的项 T
,T
中自由出现的 x
在 C[T]
中是约束变量。
注释 C
中仍可因自由出现的 x
而使 x
是 C[T]
中的自由变量。
状态不变蕴含语言规则中或可选地由实现定义的等价关系决定。
除非派生实现另行指定,约定:
若存在状态等价性以外描述的行为描述,由派生实现指定。
可观察行为如有其它外延,由派生实现指定;否则存在副作用是存在可观察行为的充分条件。
实现应满足实现行为和语义蕴含的可观察行为等价。除派生实现指定的更特定的具体行为等价性外,其余的行为等价性未指定。
实现可支持实体具有对外部不引起可观察行为差异的隐藏状态(hidden state) 。
隐藏状态和程序约定的一些状态作为管理状态(administrative state) ,以隐藏局部的状态变化对程序中其它状态的影响。
非管理状态是数据状态(data state) 。
原理
形式上,可观察的性质影响特定的项的操作等价性(operational equivalence) :替换操作等价的项得到的两个规约在可观察性质上是等价的,即两个规约的结果相等(对应行为不可分辨)。因此,可观察的性质可形式化为作为这些等价规约的结果的参数。
最简单的做法,如 [Shu10] §8.3.2 把具有可观察性质的项处理为常量语法域(syntactic domain) ,不需要附加定义相等性或影响其它规约规则。
对语义蕴含的可观察行为等价的要求指定了允许实现进行语义保持变换(semantic preserving transformation) 不能修改可观察性质的内涵,进而明确了实现对程序的可优化的界限。
数据状态和管理状态的分类类似 [RnRK] 中改变对象的性质上对状态的划分,但不仅仅应用在关于改变对象的判断上。
改变对象意义上和 [RnRK] 对应的具体实例是实体的不可变性。
注释
关于实体的状态,参见实体的等价性。
不严格要求实现行为和抽象机语义蕴含的所有推论一致。
NPL 派生实现不保证是纯函数式语言,其中的计算允许描述状态的改变。表达式的求值的作用和 [ISO C] 以及 [ISO C++] 类似。不同的是,本文档的定义明确指定控制作用的更一般外延:改变控制状态,即便这些状态并非从属一等实体。特别地,最简单的条件分支也明确具有副作用。
派生实现可定义其它的作用。
在推理实现行为时,副作用应仅在必要时引入。
作用具有种类(kind) 。值计算是作用的种类。
副作用中,对象的改变是一种作用的种类。
是否存在副作用是互斥的,即一种作用不可能同时是副作用和不是副作用。其它作用的种类可能相交,即可能同属不同的作用。
派生实现可定义其它作用的种类。
求值可引起副作用的起始(initiation) 。副作用的存在(如改变状态)可继续保持到求值结束后,并可影响可观察行为。
副作用的完成(completed) 即副作用的存在的终止(如改变状态完成)。
引起作用的求值蕴含(imply) 求值关联的作用,以及其中蕴含的副作用的起始决定的其它作用。派生实现可定义特定的求值使之蕴含的其它的作用。
作用之间可存在等价关系。等价的作用相互替换不影响可观察行为。
原理
允许派生实现定义不同的作用以维护变化的自由。
因此,副作用应仅在必要时引入,不能在推理行为时无中生有(out of thin air) ,除非证明引入的副作用不蕴含被许可的等价的实现行为以外的其它行为。通常需明确区分是否依赖副作用以避免非预期的行为。这有助于保持易预测性和可组合性。
NPL 及其派生实现中的作用可描述一般的计算作用,不限定作用的种类的外延。
明确副作用的起始是必要的,因为语言至少需要支持允许无法反馈外部状态完成改变的副作用,即 I/O 操作,此时副作用的存在应被允许保持到求值结束后,否则求值无法终止而被阻塞(blocked) 。
副作用的完成是和起始相对的概念,在讨论有关顺序时可能实用。
注释
派生实现可定义的其它的作用可能是副作用。
副作用的起始在 [ISO C++] 的关于求值(引起的作用)的规则中同样被明确。
实体是语言中主要表达的目标。
本节提供和实体相关的公共定义和语义规则,并归纳关于一等实体和一等对象的性质。
除非另行指定,语言中不引入非一等实体。仅在特定局部上下文中操作非一等实体。
原理
限制非一等实体出现的规则有助于统一性。
注释
根据一等实体和一等对象,规则 G1a 是限制非一等实体的规则的推论。
一等实体的一等(first-class) 性质体现在语言支持的操作限制足够小,使之实例的全集可以涵盖任意求值上下文中。
一个一等性质的反例是 [ISO C] 的数组类型的值无法作为函数的形式参数。推论:[ISO C] 的数组对象不是一等对象。
等价谓词(equivalence predicate) 是判断等价关系的谓词。
等价谓词可定义一些等价类划分。
语言提供等价谓词判断两个项之间是否满足等价关系,满足判断等价关系的需要。
作用于实体的值的等价谓词(若存在)定义实体的相等(equality) 关系。
注释 这类似一般的值的集合上可能存在的相等关系。
决定相等关系的谓词是相等谓词,可判断实体和实体的值相等(equal) 。
除非另行指定,默认实体上的具体等价关系是实体的同一性。
对象语言不要求提供默认的具体等价关系,即任意两个实体不一定可以比较等价。
已知可比较等价的任意实体之间的等价关系也不具有唯一性。
一般地,设计等价谓词需注意避免一些现实的使用困难,如关于相等性的困难。
为使等价关系在实体全集上良定义,等价谓词可能在特定情形弱化为同一性。
一般地,弱化应具有可用性理由,这可能和既有等价谓词和等价关系的蕴含的设计相关。
原理
等价谓词在避免依赖良序(well-ordering) 和良基(well-founded) 的理论中满足最小依赖原则,尽管其实现仍可能依赖序关系。
等价谓词的用途和上下文相关。
同一性在作为等价关系蕴含实体上的任何其它等价关系。被蕴含的等价关系可具有更多的限制条件。
实体的同一性是普遍的,但不是普适的,它仍不足以在所有上下文中都被关注。
同时,同一性无法保证在对象语言的所有实体上被实现;否则,会引起根本上显著的问题,限制语言的可扩展性,而和通用目的语言的一般属性冲突:
- 这类普遍同一性的具体判断依赖外延性,实质上是语言要求的相等性。
- 这要求任意具体的实现通过已知的、有限的方式构造,直接破坏封装性。
- 特别地,在函数等实体上的定义体现函数外延性(function existionality) ,非常依赖具体实体的构造。
- 即便不考虑实现细节,如 λ 演算这样较为简单(但足够有表达能力))的形式系统,一般的外延性即是不可判定的。
- 注释 这种不可判定使某些具体编码具有不唯一的表示,如 Church 数(en-US) 。
- 尽管有的系统如公理化集合论通过公理语义(具体地,外延公理)可以确切定义外延性,这同样依赖破坏封装的具体实现。
- 即便非通用目的语言也可能类似地因为外延性上的不可判定等问题,存在充分理由拒绝默认提供函数外延性的等价谓词。
- 这要求任意具体的实现通过已知的、有限的方式构造,直接破坏封装性。
- 进一步,为满足简单性和可用性,这自然地要求任意具体的实现是对象语言的源代码中可表达,而破坏语言的可扩展性。
- 注释 例如,语言中不能引入隐藏具体实现细节的互操作。
-
注释 作为反例,λ 演算中的对象都被 λ 项编码。但这引起的限制在通用目的语言中无法接受。
- 例如,原生的算术在算法复杂度意义上即是低效的,而通过机器数等方式的实现在复杂度和常数上都更高效。但后者的实现依赖外部系统,不能表达为 λ 项。
决定普适的等价谓词中蕴含的统一的等价关系是不可能的,因此语言中应允许共存多个等价谓词。具体等价谓词的设计可由派生实现及语言的用户提供。
等价谓词设计中弱化等价性的一个例子是 [R6RS] 的记录(record) 对象的相等性。
注释
实体的同一性是实体上的可用于定义状态不变的等价关系的例子。它蕴含了实体没有被替换为不同的实体的判断,满足保持这种判断的不变量。
关于实体的关联的值是相对同一性更弱的等价关系。因为不可分的同一性,同一实体蕴含其值相等。
一些情况部分值的集合不满足数学意义上的等价(如浮点数的 NaN ),但在此忽略这种可被单独扩展的情况。
以下不同准则的操作是相等关系的实例(参见 [EGAL] ):
- 抽象相等(abstract equality)
- 引用相等(reference equality)
- EGAL ([EGAL])
同一性是实体上的等价关系的一个主要实例。
同一性决定的等价类之间的实体相同,即其整体和任意的属性在任意上下文中等价。
相同的实体在语言中不需要被区分,可直接替换而不影响程序的语义和行为。后者蕴含可观察行为等价。
实体的同一性可体现在以下隐含的默认规则:
- 不同上下文的实体默认相互独立而不保证能被视为相同(在任意上下文中等价)。
- 通过语言构造引入的超过一个实体,默认为不相同的实体。
- 除非另行指定,表示具有同一性的实体的语言构造和其它实体不被要求共享指称相同的具有同一性的实体。
语言在一等实体上提供的同一性的具体判断依据和具体语言支持的特性相关。
原理
同一性决定任意两个实体可在语言中不依赖具体操作的行为被直接区分,即满足 Leibneiz 法则(Leibneiz's law) ,或称为不可分的同一性。
基于这个性质,可在实体上定义和 [So90] 相容的更强的(不依赖语言设计中不存在副作用的)引用透明性(referential transparency) 。
同一性的引入默认是名义的,即断言具有同一性的实体和其它实体上的行为相互独立,而不需要附加证明。这种假设避免了一般地证明任意实体具有同一性的困难。
若不依赖直接在实体上标记等价类等依赖名义同一性假设的方法,证明一个实体具有同一性而非已知的其它实体,需证明任意的其它允许在程序中构造的实体和这个实体上的任意作用的可观察行为无关。在不限定具体的计算作用属于会影响可观察行为的计算作用的确切集合时,这是计算上不可能的。因此,支持这类证明会有效地限制语言在支持不同的计算作用种类上的可扩展性。
反之,从不同的对象上取消同一性(而允许实现共享资源等目的)一般是容易的:只要证明不存在影响可观察行为的计算作用即可。这种证明可以由程序名义地表达,例如标记某个实体上只涉及纯计算而没有副作用。
另一方面,这也提示纯计算在各种计算作用中具有的特殊性不足以使其作为唯一的可扩展配置的起点。
最平凡的起点应是没有任何计算作用的空计算。这无法表达计算,而必须要求扩展才具有实用性。而通用目的语言需要支持一般的计算作用,这同时包含支持纯计算。
从一般的计算作用排除副作用而得到纯计算,只需要添加可被系统证明的假设,这种机制可以嵌入到系统的规约规则中;而以支持纯计算的系统扩展表达一般的计算,需要引入不足以被对象语言求值规则描述其语义的间接表示(即需要被规约以外的规则翻译),并暴露更多和表达一般计算的目的无关的实现细节。
注释
按不可分的同一性,实体的属性在形式逻辑中通过量化的谓词判断而实现。
和不可分的同一性相对,存在同一性的不可分性(the indiscernibility of identicals) 。两者可被二阶语言形式地描述。
对象语言可提供同一性的相关操作,如:
- [ISO C] 的非空对象指针的比较操作比较指向的相同类型对象的同一性。
- [RnRS] 和 [RnRK] 的
eq?
过程/应用子比较两个操作数的同一性。
通过特定的等价关系可定义具体的不可变状态的集合。
这些集合可用于定义以这些状态为值的实体的不可变性(immutability) ,进而定义不保持可变性的改变操作和具体的其中可能影响可观察行为的修改操作。
通过限定不同的修改操作,定义不同的可修改性(modifiability) 和对立的不可修改性(nonmodifiability) 。
通过明确不可修改性拒绝支持修改操作(例如通过通过实体的类型检查拒绝特定的修改操作),或通过不提供修改操作,语义规则保证实体不被修改操作改变状态。
注释 例如,关于 [ISO C++] 的非类且非数组类型的纯右值不可修改,尽管要求非纯右值的语义规则可被视为是一种类型检查。
(不依赖和影响实体同一性的)同一个实体上的修改操作是改变操作。只有具有可变状态的实体可能支持这些操作。
不论是否能区分同一性,实体可能关联不排除影响可观察行为的可变状态。
一般地,一个实体不一定保证可区分是否具有不可变性以及具有何种不可变性(也蕴含一般不可区分可修改性),因为不可变性依赖实体的表示进行约定。
改变操作可能继续保持实体不变。
潜在引起实体的一些内部状态的变化的操作可不被视为影响不可变性而不被视为实体的(整体意义上的)改变操作。这种实体具有内部可变性(interior mutability) 。
可引起实体变化的状态按设计上是否隐藏局部变化分为两类:
-
可变管理状态(mutable administrative state)
- 可变管理状态的改变作为管理状态的改变,不被视为对象(整体)改变的对象内部状态的改变。
-
可变数据状态(mutable data state)
- 可变数据状态的改变是对象的改变。
隐藏状态在可变性的意义上视为可变管理状态。
推论:
- 引起实体内的可变管理状态的改变的操作不一定是改变对象的操作。
- 引起实体内的隐藏的可变状态的改变的操作不一定是修改操作。
原理
基于等价关系而不是预设具体表示之间的相等定义可变性,避免抽象的目的(如封装性)依赖特定相等关系的实现细节,支持开放世界假定。
这种设计的一类典型反例是在预设排除副作用的纯的的设计为基础进行扩展定义改变操作,包括:
- 默认使用不可变数据结构,并在此基础上扩展出可变的数据结构(如 [Rust] )。
- 默认支持保证排除副作用的纯求值,仅在有限的上下文中通过特定构造模拟支持非纯求值(如 Haskell 等纯函数式语言)。
一般地,这类策略对通用目的语言是过度设计,因为这实质上要求所有不存在改变操作的实体操作都完全排除副作用,不支持指定不同类别或层次保留不同改变操作并划分不同等价类的可能性,而限制表达的能力或增加实现相同抽象的复杂性。
关联可变状态的实体通常是对象,因为支持区分同一性而能支持发生在不同实体上的作用引起独立的状态的改变而分别影响可观察行为,但这并非绝对。只要允许构造出按等价关系判断具有不相同状态,非对象实体仍可支持内部可变性等不能排除影响可观察行为的性质。这不通过需要区分同一性的状态改变。
不区分同一性允许实现任选其中的实例代替其它实例。因此,在抽象机语义上依赖这些实体的不同等价状态表现的所有良定义行为都应被允许,即未指定行为。
内部可变性同 [Rust] 的 RefCell<T>
等使用的模式以及 [ISO C++] 的 mutable
,允许对象具有可变管理状态,而不影响依赖可变或可修改的对象整体意义上的类型检查。
和 [Rust] 不同而和 [ISO C++] 更加类似,这里的内部可变性仅限关于对象不可变性,和对象是否被别名正交(一些实例分析参见这里)。
但是,和 [Rust] 及 [ISO C++] 都不同,这里不要求不可变性通过类型检查强制。
注释
可变和不可变的状态的区分类似 [RnRK] 。
其它语言也遵循类似的设计。作为非对象实体的可变性的一个例子,C++ 引用是否要求存储未指定,尽管占用存储这一状态并非是语言支持的可变状态。这一规则直接允许 C++ 实现不需要依赖 as-if 规则即可选取占用和不占用存储的方式实现引用的实例(乃至在运行时改变选取策略),即便是否占用存储可能对应 C++ 程序的不同的可观察行为。
改变或修改实体后,实体可能不变,即仍然具有和之前等价的状态。例如:
- 改变操作使用等价的状态替换先前的状态。
- 连续的改变操作使回复原始的状态,则这些改变操作的组合的作用不改变实体。
按定义,蕴含引起表达式的值以外的改变的操作的作用是副作用。这里的改变是名义的,允许改变前后的状态等价。
支持不同等价的不可变性的一个用例是,有序的数据结构中的键需要保持的(通过序关系定义的)等价关系和键的可修改性是两种不同的等价关系。作为它的一个具体的反例,C++ 标准库要求关联容器的键具有 const
修饰,没有区分两种等价性,导致无法修改等价的键(除非具有 mutable 数据成员),而引起一些不必要的复杂。
在已知的实体以外,实体,作为其副本(copy) ,满足:
除非另行指定,若实体的副本无法被创建,引起创建副本的操作引起错误。
若实体的副本可被创建,它可能通过:
- 复制(copy) 实体:创建副本后,保持原实体的值不变。
-
转移(move) 实体:创建副本后,原实体被转移而具有有效但未指定(valid but unspecified) 的状态;若可能取得实体的值,其值未指定。
- 原理 要求有效,隐含其访问时具有良定义行为。
- 注释 参见未指定状态。
- 注释 这类似 [ISO C++] [lib.types.movedfrom] 。
- 析构性转移(destructively move) 实体:创建副本后,原实体的生存期结束,不再可访问。
- 其它派生实现指定的创建实体副本的不同方式。
实体的集合上可定义关联关系:集合的包含关系或其它实现定义的严格偏序关系。被关联的实体称为子实体(subentity) 。
子实体可以是作为数据结构的一部分。这种数据结构可以是一般的图(graph) 。
数据结构也可在对象语言中通过实体包含关系以外的途径定义。
注释
例如,限定包含关系构成的图中的所有权关系附加限制,详见自引用数据结构和循环引用。
续延(continuation) 是特定上下文中描述未来的计算的实体。
续延描述的计算可能在尚未发生的后继的规约中实现,在此之前可能被调度,其中可指定不同的计算内容。上下文可决定这些计算中可变的参数化部分。
当前规约的上下文中对应的续延是当前续延(current continuation) 。
按对控制的限制,续延可分为无界续延(undelimited continuation) 和有界续延(delimited continuation) 等不同形式。
续延蕴含子续延(child continuation) 作为最后的一系列子计算。
形式上,若续延表示为一个计算步骤的序列,子续延的表示是续延的表示的后缀。
推论:无界续延的子续延是无界续延;有界续延的子续延是有界续延。
注释
续延可由符合项规约系统的规约步骤的集合或未指定的其它形式的表示。
不同形式的续延的调用都能具有类似的控制作用,但表达能力不尽相同。
有界续延可从无界续延或有界续延通过添加续延的界限(delimiter) (或称为提示(prompt) )派生。派生的结果是原续延的一部分,表达原续延对应的计算的子计算(subcomputation) ,又称部分续延(partial continuation) 。
在仅使用局部变换(local transformation) 即 Felleisen 宏表达(macro-expressible) ([Fl91]) 的意义上,[Fi94] 指出:
- 有界续延和可变状态(存储)可实现无界续延。
- 嵌入在语言中的任意表达计算作用的单子(monad) 的构造可用有界续延实现。
NPL 区分两类不同的一等实体:只关心关联(作为对象时)的值的,和同时关心作为对象的其它属性的一等对象。
其中,后者允许更多的操作,且允许作为前者使用,反之无法直接保证:一等对象总是一等实体,一等实体不保证可作为一等对象使用。
逻辑上,一等实体可以关联其它对象(作为一等对象时关联可以是存储)。关联的对象的(表达式相关的)值是一等实体关联的值,可对应一等对象存储值。关联的值或存储的值是一等实体或一等对象的属性。
除非派生实现指定,NPL 的一等实体都是一等对象。
结合实体语义相关规则,存在推论:除非另行指定,语言中不引入非一等对象。
原理
一等对象的值是一等对象的属性。
一些设计中,显式地不区分对象和值,因为这些设计中不支持普遍的一等对象。在这些设计中,一等实体被称为一等对象。因为不保证提供其它属性,一等对象的值和一等对象也不再被区分。这有助于简单性,但阻碍实体语义的可扩展性,直接无法从语言设计中允许在一等实体中区分一等对象。因此,NPL 不使用这种设计。
注释
派生实现可以定义非一等对象的其它一等实体。
除非派生实现指定,非一等对象也不是一等实体。
显式地不区分对象和值如 [RnRS] 。这些设计中,值对象若被使用,仍被作为实现细节;且因为互操作和允许支持副作用,值对象并非全部一等对象的内部表示。
一等对象通过保证具有同一性强调不相同的对象总是存在至少一种总是不相同的属性。
一般地,语言规则选取其中一种属性作为名义(nominal) 同一性属性。
一等对象具有名义同一性,定义为可比较名义同一性属性相等;名义同一性的相等即名义同一性属性相等。
名义同一性在名义上标识相同的对象,区分不相同的对象,即便后者可能仍然在行为上完全符合同一性的要求。
形式上,一等对象是名义同一性属性和它作为一等实体的关联的对象作为非对象(无视同一性)的其它属性集合(如存储的值)的二元组。
为简化设计,NPL 约定以下默认规则:
- 除非另行指定,名义同一性属性指定为对象在抽象机语义下的存储位置。
- 对象占据存储位置起始的若干存储。
- 存储位置的表示未指定;派生实现可指定具体的表示。
- 在语言规则中,一等对象满足实体的同一性的默认规则。
原理
由语言特性而非用户程序提供表达同一性的支持是必要的,这体现在通过在通用目的语言中省略同一性的表达再由实现或用户程序引入的做法一般是不可行的:
- 由 Rice 定理,非平凡(non-trivial) 的程序语义性质无法被可计算地实现,而确定程序中任意对象的同一性蕴含判定“和特定程序行为一致”这种非平凡语义性质,无法被通过证明程序行为的等价或其中的实体在任意上下文上的等价任意地引入,因此若无法确定用户程序不需要任意的同一性(这是一种平凡情形),指定“不需要引入同一性”总是只能在特定的程序上由语言设计者或用户具体地决定。
- 作为通用目的语言若需要描述能适应语言自身实现问题的特性,总是依赖具体语言的逻辑上的直谓(predicative) 的规则(如资源抽象),除非语言规则是空集(这是一种平凡情形),这不可能完全由用户程序提供。
语言的设计中显式区分一等实体和一等对象的支持而非只直接支持一等对象仍然是必要的,主要原因是:
NPL 对一等实体提供普遍的支持。
除非另行指定,NPL 不限制一等实体上可能具有的作用,包括副作用。
原理
一等实体的普遍支持体现在:
- 在一般的一等实体上引入可变状态,实质上提供了一等副作用(first-class side effect) ,而不把可修改性限于特定的数据结构(如求值环境)。
- 允许以一致的方式和实现的外部环境进行互操作,特别地,允许物理上提供状态抽象的设备实体的状态直接映射为一等对象。
注释
特别地,一等对象默认支持可变状态。
派生实现可附加规则改变本节中对一等对象的默认要求,提供不同的保证或性质,包括非一等对象上的其它一等实体上的不同作用。
NPL 中,对象的同一性关联的属性包括明确开始和终止的生存期。
推论:对象是表示能明确生存期开始和终止的实体。
一等对象之间总是能准确地判断影响程序语义的同一性:仅当能证明不改变可观察行为时,两个一等对象的同一性可能未指定。
原理
通过一等对象关联同一性,允许语言提供依赖同一性差异的特性。
注释
同一性在这个意义上不是对象自身确定的性质(而是对象和解释对象表示的可能由外部提供的实现的共同确保的性质),不是应被隐藏的内部实现,因此 [EGAL] 中有关自我诊断(autognosis) 的结论不适用;而代理(proxy) 仍然可通过语言提供适当的隐藏同一性的手段可靠地实现。
确保区分同一性的状态是一等状态(first-class state) 。
一等对象能直接表示一等状态。
一等状态是否通过其它特性派生是未指定的。
原理
一等对象相对一等实体的附加规则限制集中体现在允许一等对象映射到的支持上。
注意并非所有一等对象都需要支持一等状态;否则几乎总是会付出本不必要的代价也难以避免违反适用性;因此有必要区分一等状态的对象和非一等状态的对象。
这种区分实质上更普遍地对具体的计算操作也存在意义,自然地引入了类似 [ISO C++] 的值类别;最简单的设计如区分左值(lvalue) 和右值(rvalue) 分别关联是否需要支持一等状态的对象。
为支持一等状态,有必要支持判断两个对象的同一性,确保修改某个对象的操作不会关联到任意其它对象,以允许特定对象关联特定的一等状态。
为允许一等状态和外部环境的互操作,不能总是假定只有一类总是可被程序局部预知的修改操作(典型地,定义为“设置被引用对象”操作,如 [RnRK] §3.1 )影响状态,而应允许和特定对象关联的求值时的不透明的副作用。
若不考虑互操作,则一等对象用有限的不同等价谓词]即能提供区分同一性的操作;否则,等价谓词的设计即便保持正交,也需区分不同的一等对象对各种副作用的不同支持情况。
避免指定一等对象的可派生方式有助于统一性。
基于 [Fi94] ,结合可变状态能被表达为单子(如这里)的事实,有界续延可实现状态。
相对地,基于 [Fl91] ,无界续延和异常不能实现一般意义的可变状态,参见这里的推论 5.13 。
因为同一性可以在引入状态时被编码而在之后不需改变,使用有界续延等非一等的状态可支持实现状态的同一性。因此,在此不对是否基本要求作出限定。
但是,使用有界续延实现状态仅仅是实现细节,且通常具有一些非预期的实现性质:
- 这在控制状态和支持一等状态的实现之间建立的不对等(地位不同,相互之间交换后不等效)的偶然耦合;这种耦合不存在简化实现等益处而具有必要性。
- 注释 例如,一等状态可能直接使用对应的寄存器(register) 实现。实现控制状态则通常需要更复杂的实现。
- 尽管理论可行,没有必要只是用其中一种作为另一种的实现的基础实现。
- 在现有实现普遍提供状态的原生支持(存储器)的常见情况下,单独通过其它方式编码状态反而会付出本不必要的代价。
- 这实质要求实现同一性无界续延具有区分同一性的能力(相当于 [ISO C++] 的左值),而引起不正交的内部设计。
为满足非常规的实现环境或更优先的原则(如变化的自由和正确性),派生实现仍可使用有界续延派生一等状态,同时提供访问更基本的不依赖可变状态的接口,以使上述影响不再是非预期的。
用户程序仍不被禁止使用这种方式自行提供类似的实现,以确保不约定一等状态作为基本的内建特性时,语言的设计不违反 G1b 。
注释
实现在一般实体上支持的隐藏状态不被程序可编程地指定,不是一等状态。
允许和特定对象关联的求值时的不透明的副作用的一个实例是 [ISO C] 和 [ISO C++] 的 volatile
类型对象。
语言可指定特定的求值自动创建对象。
基于此规则可在传递时附加不同的作用,即实现可随一等对象传递的一等作用(first-class effect) 。
原理
典型地,按值传递时,被传递后的对象和实际参数表示的对象具有不同的同一性,即按值传递时创建新的对象。
基于被创建的副本的不变性,这里的一等作用可包括用于维护对象的不变性的作用,包括可能的副作用,作为契约式编程(programming by contract) 的基础实现方式。
这种不变性可包括对象的生存期。通过限制特定表达式求值的作用域内销毁对象以确保对象生存期有限,即基于作用域的对象管理(scope-based object management) 。
基于作用域的对象管理可直接对应有限资源的普遍性质,使一等对象作为资源的抽象,确保资源的创建和销毁的副作用符合资源操作的语义,同时避免隐式的泄漏。
配合一等状态,对象语言中的一等对象允许直接表示超过程序运行时自身的生存期的状态。这允许不在程序运行时持久储存的数据能直接被一等对象进行操作,而不需要依赖外部系统的约定并减少冗余操作(例如,从外部持久的“文件”上打开“流”以及其上的持久化操作),更符合简单性。
注释
这里的资源抽象的惯用法在 C++ 中称为 RAII(resource aquisition is initialization) 。
配合一等作用,实体的所有权(ownership) 自然地适用对抽象为对象的资源进行约束。
使用对象代表资源,则所有者(owner) 约束被其所有的其它对象的创建和销毁的时机。被所有的对象的生存期是所有者的生存期的并集的子集,且:
NPL 的设计避免要求对象语言隐含单一的根(root) 所有者作为其它资源的所有者。
原理
避免单一所有者适应抽象不同系统的需要,并满足变化的自由:
- 当不需要这样的所有者时,保持设计的简单性,同时满足避免不必要付出的代价和最小接口原则。
- 当需要这样的所有者时,仍然允许实现或派生实现引入。
注意规约允许蕴含非一等对象的所有者用于提供规约时不在对象语言中可抽象为一等对象访问的资源,这样的所有者不需要是全局的;若实现为在不同规约实例乃至全局共享的资源,也不应在对象语言被依赖。
只要程序没有明确要求所有者,单一的全局所有者违反最小依赖原则,且不支持不清楚所有者状态时对特定对象之间进行所有权的局部推理(local reasoning) :
- 这种情形若不配合原始的明确目的(而间接明确资源的所有者)的设计说明,人类读者直接阅读实现理解和验证其正确性是困难的,即损失了可读性。
- 一种解决方式是读者自行模拟运行程序再从中推理出可简化的资源所有关系,这首先相当于要求读者模拟非确定性垃圾回收(GC, garbage collection) 的运行机制。这通常是困难的工作。
- 而机器通常更无法推理这些问题,因为设计和抽象的目的一般不是以机器可读的方式编码的。
- GC 可以回收资源,但无法准确统计哪些回收是必要的,也无法准确追溯原始实现并推理出应当在何种情况下静态地插入释放资源的操作,因为 GC 自始至终缺乏“允许任意延迟释放操作”以外的程序变换的保持语义不变的证明所需的程序元信息(包括目的)。
为满足变化的自由,当需要表达局部所有权关系时,使用单一的全局所有者使用户无法直接在对象内嵌(embedding) 这种关系而需另行编码所有权信息,这存在以下问题:
- 使整体设计直接违反避免不必要付出的代价。
- 要求局部所有权以和全局默认机制的不一致的方式表达,损失统一性并放弃局域性而在满足需求时造成接口抽象泄漏。
此外,即便使用时不要求区分对象的局部所有权关系,全局的分配释放机制也比局部的机制有更大的实现复杂性和约束。为实现对内部有限的资源的有效管理,局部所有权在实现中仍是必要的。
在使用全局所有者如全局的垃圾回收的实现中,这种必要性被隐藏在全局所有者内部实现,语言的整体设计不会更简单。
使用全局所有者的资源管理假定启发式(heuristic) 策略以节约现实中无法接受的非预期性开销。这仍无法保证总是对不同的场景同样有效,以至于默认存在以下问题:
- 设计至少违反变化的自由和简单性之一。
- 在不引入支持用户配置策略的扩充设计时,违反变化的自由总是无法避免的。
- 若引入其它设计支持用户配置策略,简单性违反难以避免,且实际基本上没有被避免。
- 即便能通过扩充设计避免违反简单性,也不能避免不必要付出的代价。
- 不论是否引入扩充设计,都会使资源管理的一般开销更难以估计,而使设计整体的可用性评估更困难,容易使用户决策和避免不必要付出的代价冲突。
NPL 的一等对象即对象自身,不要求区分引用和被引用对象(referent) 的普遍概念。
反之,通过使引用和其它一些非引用的对象同为一等对象,NPL 支持作为一等对象的一等引用(first-class reference) 。
一等引用支持一等对象作为被引用对象。除非另行指定,若实现允许非一等对象作为被引用对象,可作为被引用对象的非一等对象由实现定义。
特定的操作可能预期非引用,或总是隐含通过引用访问被引用对象,这不改变引用被作为一等对象使用的普遍支持。
一等对象的使用仍然可以通过要求引用访问以避免在任意上下文中需要不同的对象副本。但这并不应排除其它形式的一等对象操作。
原理
尽管满足 [RnRK] Appendix B 的准则(criteria) ,一等对象和 [RnRK] 及 Java 等语言要求的设计不同。
注意有引用的语言的语义中不能排除被引用对象,否则无法确定引用对象的值的表达式的求值结果(例如来自对象存储的值)以表达计算;相反,无视引用而直接对值操作仍然能实现一些足够有意义的程序。
因此,若存在引用,无法忽略非引用(即便非引用不能在对象语言被直接使用)。
另一方面,引用可以由不指定为引用的一般对象上添加语义规则区分,而作为一般的对象的特例。
要求语言操作的一等对象总是关联到引用的设计实质上使对象语言的一等对象都是引用。但这不表示引用是自然的一等实体,因为引用的作用仅是操作被引用对象,不要求引用自身能被作为一等对象。
一等引用的相等性定义允许在相等的引用上推理引用透明性。
考虑此设计决策时关注的有以下几节中的依据。其它依据参见 YSLib 项目文档 doc/NPL.txt
。
共享引用是共享资源的引用。共享的资源(通常是存储空间)自身具有同一性,以位置(location) 标记。共享不同位置(即作为不同一等对象的)的引用可能引用同一个被引用对象。
原理
合理的共享引用可以节约实现占用的资源,提供更好的性能。但共享引用的实现仍可能有附加的开销,因此并不能保证使用共享引用一定能提供更好的性能。通常这种情形至少包括一些典型的对资源独占一次使用(具有独占所有权(unique ownership) )的情况。
更重要地,并非任意引用的共享都不改变程序的语义和行为,不合理的使用可能造成非预期的作用。
任意地引入共享引用而使用户不便预测其作用破坏适用性:
- 这包含直接破坏易预测性,并在需要排除共享的场景中缺乏可用性。
- 特别地,这和具有副作用的非确定性(non-deterministic) 编程冲突。
- 典型的多线程并发执行若需对象上的副作用,需要保护和排除不必要的共享,确保独占所有权以避免竞争条件(race condition) 。
- 也有其它的一些类似的容易被忽略的非确定性地破坏假设的场景。
- 另见共享改变。
区分是否需要表达共享的情形一般不能由语言实现预知。和使用全局所有者的问题类似,使对象默认共享的设计若需避免违反避免不必要付出的代价,在此相对不默认共享引用的设计违反简单性。
默认共享引用可能是隐式的,即语言的实现不通过程序代码中的显式标注的操作而引入共享的引用,且往往无法保证通过一等对象上的操作避免被引用的对象被其它一等对象引用——无法使用对象语言的操作排除共享引用(即便是新创建的对象也没有保证,尽管实现上不必要)。
在要求一等对象都是引用的设计中,一般地,只有不要求名义同一性的非对象的实体才能安全地共享引用,但在非对象实体上的类似引用的机制并没有保证通过一等引用提供为语言特性。
其它情形中,允许引用之间的隐式的共享使不相同的对象可能共享状态而破坏同一性的行为保证:程序无法可靠地避免共享状态导致的对可观察行为的影响,此时共享状态的改变非预期地影响其它对象,其行为不具有一致性。
为了排除破坏同一性和适用性的问题,语言的设计需要限制引起问题的操作的可用性(例如,[RnRK] 和 [RnRS] 不提供使用一等引用的改变操作以保证变化能通过程序源代码中有限的语法上下文被推理),但这样的策略限制设计的通用性。
因为共享引用的影响的普遍性,不提供可避免隐式共享引用的设计的造成的缺陷也是普遍的。
由于显式的引用可以由用户控制在局部使用,更容易推理其影响,可避免类似的缺陷。
关于共享改变和程序无法可靠地避免共享状态导致的对可观察行为的影响,参见参见 YSLib 项目文档 doc/NPL.txt
。
注释
一些语言的设计指定或隐含的规则在程序代码操作的一等对象上普遍地引入隐式共享的引用,如:
[RnRK] 中的引用和被引用对象明确地分离,且 $define!
和 set-car!
等改变操作要求设置对象引用的其它对象为特定的操作数确定的被引用对象,无法排除被设置的引用被共享;这实质要求所有可能包含其它引用的可被改变的对象中的引用都需要能构成隐式的共享。
[RnRS] 明确指出特定的空对象的唯一性(即便因为不保证具有位置,不一定保证以位置决定的名义同一性),蕴含这些对象上总是可构造或超过一个引用必须构造隐式的共享引用;其它变量引用(variable reference) 未指定排除隐式的共享。
除非在语言规则中添加复杂的约束(如通过类型的机制)以证明特定上下文可避免共享引用,无法避免引用引入不必要的对象别名(aliasing) 。
若公开这样的性质作为接口约束,违反最小接口原则。
隐式的共享使涉及修改的操作的特性更难设计,参见共享改变。
关于共享改变,参见 YSLib 项目文档 doc/NPL.txt
。
原理
对象别名一旦引入,通常难以在所有被别名的对象生存期结束前消除。
证明对象不被别名是困难的,因为这逻辑上要求在局部知悉所有被别名的对象的存在性,而不具有局域性。
特定的数据结构在逻辑上要求内部具有相互指涉的引用,即自引用(self-referencing) 。
自引用可实现为一等对象集合内的循环引用(cyclic reference) ,即允许对象属于有限次迭代访问被引用对象的操作的传递闭包(非空的链(chain) ,称为引用对象链)的构造。
NPL 的不保证支持这种方式实现自引用。
原理
NPL 的设计不保证支持通过循环引用实现自引用,以避免一些固有缺陷。即便派生语言允许提供扩展支持,但本节讨论的原理仍然适用。
避免自引用的构造使实体构成的数据结构由一般的图退化为(可共享节点的)树形数据结构,即 DAG(Directed Acyclic Graph ,有向无环图)。
这样的设计在实现上避免外部所有者(如全局 GC )。
避免一般的循环引用的普遍理由是:非直谓性(impredicativity) 并非是抽象上必要的普遍特性。一般的循环引用在抽象上即应通过特殊进行归纳,这并非泄漏抽象。
反之,需求决定的抽象上不必要的情形下,假定循环引用的存在反而妨碍抽象的构造,可能避免某些有用的普遍性质(例如,保证程序可终止;另见强规范化性质),而违反简单性、统一性和适用性,并引起若干具体设计问题。
关于通过任意对象支持循环引用的问题,参见 YSLib 项目文档 doc/NPL.txt
。
通过一些替代原语,在不支持循环引用的情形仍可支持自引用数据结构。
语言可以提供在不支持一般的循环引用的对象构造中保存无所有权的一等实体引用其它实体,构造出不蕴含所有权的仅以特定对象构成的循环引用,而在外部引入对象作为所有这些构成引用的对象的所有者的机制。
在这个基础上,一般的自引用或循环引用需要的附加指涉仍然可通过添加不蕴含所有权语义的引用解决。这些引用是弱引用(weak reference) ,区分于具有所有权的引用是强引用(strong reference) 。
强引用总是可转换为弱引用使用。弱引用通过解析(resolve) 取得强引用。解析可能失败,以允许弱引用指涉已经不存在的对象,而避免影响对象生存期和所有权关系。
若支持这种受限形式的循环引用,具体特性由派生实现定义。
原理
没有理由表明通过任意对象支持循环引用是自引用数据结构的唯一实现方式,不论使用自引用数据结构的普遍程度。
自引用数据结构可通过在更高的抽象层次上编码,转换为由用户(而不是语言实现)指定明确的外部所有者的形式消除上述所有问题,同时对外部保证同等的功能正确性。
使用受限的循环引用同时避免带有所有权的循环引用也是 C 和 C++ 等语言惯用的实现图(graph) 的数据结构的合理方式。
NPL 不要求预设具体的实体及对象类型的设计,因此不要求用户使用语言体现整体上的可扩展性。
特别地,NPL 不要求表达式具有预设的不同类型。
原理
放弃对预设类型的要求允许由派生实现指定类型的外延而满足变化的自由。
除不必涉及引用外,[RnRK] 中定义的封装的(encapsulated) 类型的概念及类型封装性( [RnRK] 原则 G4 )仍然适用,且一般仍然需要满足;差异是派生实现因为扩展不满足的情形也不影响此实现的一致性(尽管使用扩展的程序可能不可移植)。
尽管值类别可抽象为特殊的类型,表达式中的对象的类型和值类别的规则应分别讨论,因为两者正交:两者的确定和检查机制都相互独立。
具体的外延由派生实现定义。
表示名称的表达式不同于名称,但在无歧义时,语言中可直接以名称代指表达式和对应的词法元素。
求值算法中对名称的处理应满足本节的要求。
原理
名称规则约定通过程序源代码确定的静态的语法性质。
部分规则中的概念定义和约定仅为便于描述这些性质。和这些约定对应的结构不一定需要在求值算法的实现中出现。
对引入名称 n
的声明 D
,对应的声明区域始于紧接 n
的位置,终于满足以下条件的记号)
(若存在)或翻译单元末尾(不存在满足条件的记号 )
):
- 记号
)
和与之匹配的记号(
构成的表达式包含D
。 - 此记号之前不存在满足上一个条件的其它的记号
)
。
名称隐藏规则:若声明 D
是表达式 E
的子集,且不存在 D
的子集声明同一个名称,则 D
声明了有效名称,隐藏了 E
中其它同名的名称。
在声明区域中,没有被隐藏的名称是可见(visible) 的。有效名称实质蕴含可见名称。
名称解析(name resoultion) 是通过名称确定名称指定的实体的操作。
不保证名称解析总是成功。
除非另行指定,成功的名称解析没有副作用。
除非另行指定,直接作为求值算法步骤的不成功的名称解析引起错误。
一般地,名称解析包括名称验证(name verification) 和名称查找(name lookup) 两个阶段。
名称验证确定名称是可见名称,同时可能排除部分无效名称。
名称查找进一步确定名称唯一指称的实体的(蕴含确定名称有效),仅在名称验证成功后进行。
不同名称经过名称查找的结果可能等效。等效的有效名称视为同一的,规则由派生实现定义。
名称解析从保存名称的目标中查找名称。若查找失败,解析可继续从替代的其它目标中进行。这种机制称为重定向(redirection) 。重定向后的解析可继续包含名称验证和名称查找的步骤。
以上约定以外的具体规则以及失败的行为由派生实现定义。
命名空间(namespace) 是实体。命名空间可以由名称指称。
是否实现命名空间为程序中可由用户指定可变的实体及求值环境,由派生实现定义。
总是没有名称指称(denotation) 的命名空间是匿名命名空间(anonymous namespace) 。
没有有效名称指称的命名空间是未命名命名空间(unnamed namespace) 。
NPL 定义一个抽象的匿名命名空间,称为根命名空间(root namespace) 。未命名命名空间的支持由派生实现定义。
NPL 约定一个在实现中的有效名称总是指称一个命名空间。有效名称指称的命名空间的同一性和有效名称的同一性对应。
注释
匿名命名空间和未命名命名空间不同。前者可能是一个系统的默认约定,一般整体唯一存在(如全局(global) 命名空间);后者只是对某些接口隐藏,可以有多个。
除了用于指称的名称外,一个命名空间可以和若干其它名称关联。
通过派生实现定义的对命名空间的操作可以取得的名称是这个命名空间的成员(member) 。
若无歧义,命名空间的成员指称的实体也称为这个命名空间的成员。
命名空间直接包含成员,称为直接成员。
除了根命名空间和其它派生实现定义外,命名空间可以作为另一个命名空间的成员,此时命名空间内的成员(若存在)是包含其的命名空间的间接成员。
命名空间对成员的直接包含和间接包含总称为包含,是反自反的、反对称的、传递的二元关系。
命名空间的直接成员的标识符在这个命名空间中是有效名称,称为简单名称(simple name) 。
命名空间及其成员按包含关系依次枚举标识符组成的序列是一个名称,称为在这个命名空间中的限定名称(qualified name) 。
根命名空间的限定名称称为全限定名称(fully qualified name) 。
限定名称的语法由派生实现定义。
注释
限定名称的语法的一个实例是标识符之间作为逻辑上的分隔符的记号。
对象语言的操作语义可通过作为计算模型的项规约系统的规约规则中由规约规则描述的规约步骤(step) 指定。
除非派生实现另行指定,规约蕴含 NPL 程序的执行,可完全表示程序执行的语义。
推论:NPL 规约规则形式地蕴含 NPL 语义规则。
为表达明确的目的,语言规则也可约定其它更抽象形式的求值规则,以蕴含这些规约规则,而不是直接描述规约规则的形式语义。
描述 NPL 对象语言的操作语义也可被视为特定的对象语言,其规约可以视为求值。但除非另行指定,以下表达式仅指对象语言的表达式,其求值仅指关于对象语言中表达式的求值,而非一般的规约。
规约规则可要求被规约的项符合一定的结构(如具有特定类型的值)作为前提,否则规约出错,程序执行中止。
根据规约规则描述的行为是否对应对象语言中的求值,规约分为两类:表达式的求值规约和管理规约。
一个规约可以描述表达式的求值。直接表达一个表达式求值的规约是一个求值规约。
以项重写系统描述,求值规约的输入是作为表达式的表示的项,称为待求值项(evaluating term) 。
待求值项经求值规约取得求值结果。
除非另行指定,求值结果是通过值计算取得的值。
原理 求值结果也可能是异常退出的等其它作用对应的实体。这些求值结果可能需要派生实现定义的不同规则的处理。
以下项称为被规约项(reduced term) :
求值规约规则构成对象语言的求值算法(evaluaton algorithm) 。
求值算法的输入是被求值的表达式和支持上下文相关求值中的上下文。
求值的基本操作以满足特定规则的替换(substituion) 规则或其组合表示。
除非另行指定,以下讨论的排除求值副作用的重写系统具有汇聚性。
这保证求值满足值替换规则:表达式的值计算通过已知的子表达式的值替换决定。
除非派生实现另行指定,子表达式的值仅由求值得到。
注释 此时递归蕴含规则中的求值依赖规则是这个规则的推论。
求值规约以外的规约称为管理(administrative) 规约。
管理规约可以是一个不完整的求值规约,或者和求值规约的步骤没有交集。
管理规约可使用对象语言不可见和不可直接操作的非一等状态的管理状态。
表示非一等对象的项的规约总是管理规约。
抽象求值中不在对象语言求值结果中可表达的中间规约是管理规约实现。
注释
管理规约描述语言的表达式以外的操作语义。
实现也可使用的管理规约描述特定于实现的(而在对象语言中未指定的)语义性质。
先序(sequenced before) 关系是两个规约之间存在的一种严格偏序关系,对实现中规约之间的顺序(order) 提供约束。
后序(sequenced after) 是先序的逆关系。
非决定性有序(indeterminately sequenced) 是先序或后序的并集。
无序(unsequenced) 是非决定性有序在求值二元关系全集上的补集。
规约规则的顺序直接适用于求值,其顺序为求值顺序(evaluation order) 。
规约规则的顺序也适用在能以其形式描述相对顺序的事件(event) 上。程序中蕴含的这些事件称为规约事件(reduction event) ,包括:
一些事件的顺序是通过推理具有因果性(causality) 的依赖(dependency) 关系决定的,包括:
- 规约中值计算依赖规约的输入,即被求值的表达式和其它可能影响规约的状态。
- 被副作用的起始决定的其它作用依赖这个副作用。
- 从一个实体上确定作为值的属性的读(read) 依赖这个属性。
- 在一个实体上可以作为值保留的属性的写(write) 被这个属性依赖。
- 由派生实现定义的其它情形。
注释 外部表示作为实体的读取和写入是这里的属性的特例。
为了确定相关的值,依赖关系可直接替换为后序关系。
由二元关系的一般性质(特别地,偏序关系的传递性),可推导其它一些事件之间的确定顺序,如同一个实体属性上的读依赖(已知的)决定了这个属性的先前的写。
作为先序和后序的扩展,规约事件可符合在先发生(happens before) 和在后发生(happens after) 的严格偏序关系,满足:
- 对同一个执行线程中的事件,在先发生和在后发生分别同先序和后序。
- 组合在先发生或在后发生的关系的不存在环(cycle) 。
- 派生实现定义的其它要求。
NPL 约定以下非决定性规约规则:除因果性和二元关系的一般性质的推论外,任意项之间的规约之间无序。
应用在求值顺序上,有以下推论(非决定性求值规则):除因果性和二元关系的一般性质的推论外,任意表达式的求值之间无序。
原理
在先发生和在后发生可描述系统中的并发的事件。原始定义包括对时钟(clock) 的抽象,但此处不要求指定。
[ISO C++] 和 [Rust] 等使用类似的方式描述并发的求值的支持。这些设计中,不同执行线程中具有特定的操作定义具体的顺序关系。其中具体规则的设计可能不同而不保证完全一一对应。
因可扩展和简单性 NPL 不在此明确指定此类具体操作,而由派生实现定义。
及非决定性规约规则允许在语言中表达并发实现。
注释
读和写作为影响可观察行为的事件结果,具有因果性。此外,也可以抽象为计算作用并由程序操作;这里不做要求。
两个具体求值等价,当且仅当两者的作用相等。
两个求值等价,当且仅当作为具体求值时等价,或其中每个求值的变换实质蕴含另一个。
没有副作用的求值是纯的(pure) 。
注释 推论:纯求值仅有值计算或抽象求值。
值为被求值的表达式自身的具体求值或不包含变换为存在不等价求值的表达式的抽象求值为恒等(identity) 求值。
恒等的纯求值是空求值(empty evaluation) 。
作用是空集的表达式求值是空作用求值(null effect evaluation) 。
注释 推论:空作用求值是空求值。
语法形式固定且求值总是空求值的表达式是空表达式(empty expression) ,这仅由派生实现可选提供。
规范化形式(normalized form) ,或简称范式(normal form) ,是由派生实现定义的表示,被一组规约规则确定,满足:
- 通过有限的规约步骤后得到。
- 按规约规则,规范形式上不存在不和空求值等价的进一步规约。
在具有 Church–Rosser 属性的重写系统中,一个对象若具有范式则唯一。
表达式在得到规范形式后规约终止,且蕴含求值终止。
得到范式的规约步骤称为规范化(normalization) 。
若表达式规约总是能得到规范形式(求值总是能在有限规约步骤后终止),则具有强规范化(strong normalization) 性质。
实现应避免引起对象语言的语义表达以外的无法保证强规范化性质的操作(如直接无条件的递归规约调用)。
除非派生实现另行指定,不保证强规范化性质。
保证得到范式的规约是规范化规约。
具体求值得到的范式若可作为表达式,其求值结果是和被求值的项等价的表达式的值,即仅允许恒等求值而仍是范式;这样的项称为自求值项(self-evaluating term) 。
作为表达式的自求值项是自求值表达式(self-evaluating expression) 。
重复求值直至取得自求值项的求值结果是最终求值结果(final evaluation result) 。
注释
推论:最终求值结果上可能的求值是纯求值。因此,取得最终求值结果后,即排除具有副作用的继续求值。
第一个子表达式(头表达式)是范式的表达式是 HNF(Head Normal Form ,头范式)。
头表达式是可直接求值为范式的表达式是 WHNF(Weak HNF,弱头范式)。
注释 约定求值到 WHNF 提供保证强规范化性质的一般手段,可用于非严格求值。
WHNF 的头表达式是操作符(operator) ,存在对应 HNF 的头表达式的最终求值结果。
注释 详见合并子。
WHNF 中除了操作符以外的子表达式是操作数(operand) 。
操作数以具有限定顺序或不限定顺序的数据结构表示。
按操作数的数据结构对应有操作数列表(operand list) 和操作数树(operand tree) 。其中操作数树是有限的树形数据结构的 DAG ,其具体构造和表示由派生实现定义。
注释 操作数树和 [RnRK] 类似。语言可能进一步约定有序的数据结构表示操作数的组成部分之间在求值上不等价。
这种能以操作符和操作数的组合表达的计算形式是操作(operation) 。
操作的结果(result) 是表达规约步骤得到的范式;操作的作用是取得对应结果的规约步骤的作用。
注释 函数合并的求值结果中可蕴含操作的结果,也可具有其它作用。若操作的结果存在,则同时是这个合并子的调用的结果,即返回值。
若操作的结果不依赖管理规约,操作的结果和作用即这种可求值为 WHNF 表达式的求值结果和作用。
注释 另见函数值。
关于 DAG ,参见 YSLib 项目文档 doc/NPL.txt
。
表达式和子表达式之间的求值需满足一定约束。
表达式和子表达式之间的求值满足以下递归蕴含规则:
- 求值依赖规则:除非另行指定,表达式被求值实质蕴含子表达式被求值。
- 顺序依赖规则:求值子表达式的值计算先序所在的表达式的值计算。
- 平凡求值规则:指定一个表达式是纯求值或空求值对应实质蕴含其子表达式的求值被指定为纯求值或空求值。
注释
一般地,一些求值策略可以不遵循求值依赖规则。
顺序依赖规则是因果性的具体表现之一。对不被求值的表达式,此规则不生效。构造不同的表达式进行计算可实现和直接违反此规则等效的作用,但因为是不同的表达式,实际上不违反此规则。
附加的顺序依赖规则可由特定的实体构成的表达式的求值隐含指定。相同的表达式可能在不同上下文中使用不同的规则。
若表达式的任意子表达式的求值总是非空求值且先序表达式求值,则这个表达式的求值是严格的(strict) ;反之,求值是非严格的(non-strict) 。
推论:严格求值满足顺序依赖规则。
非严格求值在规约时可保留未引起作用(通常即未被求值)的部分子表达式,允许实现根据先序的求值作用确定的选择性求值,即包括未指定是否作为空求值的子表达式求值,如分支判断或短路求值。
注释 例如:ISO C++ 的条件表达式存在可能未被求值的操作数,属于非严格求值;++
表达式不作为完全表达式(full expression) 时,副作用可超出此表达式的求值(不满足顺序依赖规则),也是非严格求值。
表达式经过严格性分析(strictness analysis) 确定是否严格求值,通过严格性分析器(strictness analyzer) 在语义分析时实现。
中间值(thunk) 是保留不直接实现具体求值的部分子表达式的特定的数据结构。
注释 例如,通过保留中间值待延迟求值,可实现子表达式值的按需传递。
明确的词法顺序可为同一个表达式的若干子表达式提供一致的有序求值策略:从左到右或从右到左。为一致性,不需要考虑其它特定顺序作为一般规则。
递归文法表示的表达式和子表达式之间存在相对内外顺序:子表达式在表达式的内部。此求值顺序可对应表达式树的遍历顺序。
对应项的规约规则的表达式的重写规则由派生实现定义,典型的可选项包括:
- 名称替换:保证替换前后项对应的名称不变。
- 实体替换:保证替换前后项关联的实体不变。
- 值替换:保证替换前后项关联的表达式的值满足实现定义的相等关系。这包括以下不同的变体:
组合严格、顺序求值和替换策略可得到不同性质的求值策略。
除非派生实现约定,表达式求值策略可以随具体语法形式不同而不同。
典型性质组合如下:
- 严格求值:
- 应用序(applicative order) :以最左最内(leftmost innermost) 优先的顺序求值。
- 最左的顺序仅在操作数是有序数据结构时有意义;不考虑操作数内部构造时,仅表示操作数作为子表达式总是被求值,和严格求值等价。
- 按值传递(pass by value) :使用值替换的严格求值。
- 共享对象传递(pass by shared object) :使用的共享机制以及对象和值或引用的关系由派生实现定义。
- 部分求值(partial evaluation) :允许求值分为多个阶段(phase) 分别进行。
- 应用序(applicative order) :以最左最内(leftmost innermost) 优先的顺序求值。
- 非严格求值:
- 正规序(normal order) :以最左最外(leftmost outmost) 优先的顺序求值。
- 最左的顺序的意义同应用序。
- 按名传递(pass by name) :使用名称替换且保持作为名称的表达式最后被替换的求值。
- 按需传递(pass by need) :按名传递但允许合并作用相同的表达式。
- 正规序(normal order) :以最左最外(leftmost outmost) 优先的顺序求值。
-
非决定性求值:
- 完全归约(full reduction) :替换不受到作用之间的依赖的限制。
- 按预期传递(pass by future) :并发的按名传递,在需要使用参数的值时同步。
- 乐观求值(optimistic evaluation) :部分子表达式在未指定时机部分求值的按需求值,若超出约定时限则放弃并回退到按需求值。
应满足的本节上述约定的最小求值规则和语义外的具体求值的规则和语义由派生实现定义。
派生实现的求值可满足以下节指定语义,此时应满足其中约定的规则。
若可选求值规则逻辑上蕴含规约规则,则被蕴含的规约规则的直接表述可在语言规则中被省略。
在被求值的表达式以外,对应的规约规则在实现此规约的元语言中可能是上下文相关的,这种附加依赖的上下文为求值上下文(evaluation context) 。
求值上下文被作为元语言实现对象求值规则时的输入,可指定项所在的位置等不被被规约的项必然蕴含的附加信息。
由派生实现定义的特定求值上下文称为尾上下文(tail context) 。以尾上下文求值可提供附加的保证。
作为项重写系统的上下文的实例,元语言中,一般的求值上下文 C
形式化为具有占位符 []
和可选前缀 v
及可选后缀 e
的递归组合的串:
C ::= [] | Ce | vC
其中 e
是被求值表达式,v
是作为范式的值。
除非另行指定,NPL 对象语言的求值算法使用的求值上下文总是求值环境。
原理
通过附加适当的求值规则保证对象语言中的表达式总是可唯一地被分解为这种表示,抽象的求值上下文可直接实现对象语言的求值。但语义描述和实现的基准都以抽象机替代,因为:
- 抽象机语义允许不依赖源程序的表示和构造(如特定的表达式的文法)。
- 这种分解一般要求遍历对象语言的源程序而难以具有较好的可实现性质,如计算复杂度。
- 为满足良好的可实现性质,需描述实现中可能具有的离散状态与只和其中个别状态关联的局部的求值规则时,这种分解通常会渐进演化为某种抽象机的表示。
注释
使用求值环境作为默认的上下文确保一般的求值总是能支持变量的绑定。
对象语言的实现同时能够支持其它上下文,即使它不在求值算法中出现。这样的上下文可能被求值上下文蕴含而可被推理确定。
作为通用语言,求值规则表达的系统可具有和无类型 λ 演算对应的形式和计算能力。
基于此语义的派生实现应允许以下几种互不相交的表达式集合:
NPL 不要求以上表达式中函数以外的表达式求值的强规范化。
注释
无类型 λ 演算保证名称表达式(变量)和函数( λ 抽象)的规约的强规范化,但不保证函数应用规约的强规范化。
扩展的 λ 演算(如简单类型 λ 演算)可保证规约函数应用的强规范化。
名称表达式(name expression) 是表示变量的 λ 项。
原子表达式的由派生实现定义的非空子集是名称表达式。其它作为名称表达式的表达式语法形式由派生实现定义。
名称表达式不被进一步规约;其求值是值替换规则的平凡形式。
函数(function) 是一种参与特定规约规则的实体,也可以指求值为函数实体的表达式。
一般地,函数表达式在 WHNF 下作为操作符被求值,其最终求值结果为函数实体,或函数对象(若函数在语言中允许作为对象)。
NPL 中,作为一等对象的函数表达式的最终求值结果是合并子。
一个函数表达式是以下两种表达式之一:
- 保持等价地求值到其它函数表达式上的名称表达式,称为具名函数表达式(named function expression) ,简称具名函数(named function) 。
- 满足本节以下规则的由派生实现定义的匿名函数表达式(anonymous function expression) ,简称匿名函数(anonymous function) 。
函数应确定替换重写规则被替换的目标,即函数体(function body) 。
除非派生实现另行指定,函数不需要被进一步规约,此时其求值是值替换规则的平凡形式。
在类型系统中,函数可被指派函数类型(function type) 。函数类型能蕴含参数和结果的类型。
注释 例如,在简单类型 λ 演算中,函数类型是类型构造器 →
组合输入(参数)和结果(输出)类型的复合类型。
匿名函数可以显式指定(绑定(bind) )包含若干变量使之成为约束变量的语法构造。
通过创建函数时的显式的语法构造引入的这种变量称为函数的形式参数(formal parameter, parameter) 。
除绑定外,匿名函数蕴含上下文可以捕获若干在函数体以外的同名的自由变量。
通过绑定或捕获引入的变量允许在函数体中允许使用。
使用词法作用域时,若匿名函数所在作用域的存在同名的名称,则被捕获的名称被隐藏。形式参数隐藏被捕获的变量名。
派生实现的语义规则应满足和 λ 演算的语义的 α-转换(alpha-conversion) 规则不矛盾。
注释 Vau 演算在没有限定环境时不考虑一般意义上的自由变量。
函数应用的求值决定被绑定的变量和函数体内的变量之间的关系,参见函数合并。此时,求值策略蕴含的替换策略蕴含被绑定的变量和函数体内的变量之间的同一性。
类似地,在被捕获的变量到函数体内捕获的变量之间,也有和替换策略一一对应的不同捕获策略。
除非另行指定,变量被按引用捕获(captured by reference) 而非按值的副本捕获(captured by value copy) ,即通过捕获引入的变量是被捕获变量的引用而不是副本。
原理
捕获为引用而不是副本,保持被捕获的变量和函数体内同名变量的同一性,在实体是对象时不影响可观察行为。若这些捕获未被使用,可被实现直接移除。
过程(procedure) 是操作符具现的可调用(callable) 的实体,决定特定的可提供求值的作用(包括决定求值结果)的计算。
函数表达式的最终求值结果由过程实体的作用中的结果决定,以派生实现定义的方式关联。
通过函数表达式可指定可选的实际参数,发生过程调用(call) 。过程的调用蕴含计算。
过程中和过程外的计算的组合满足因果性:
主调函数(caller function) 等调用者(caller) 或其它引起过程中的计算的实体转移计算蕴含的控制到过程中的计算而使之进入(enter) 到被调用者(callee) 的过程。
过程可能被限制只有一次(one-shot) 调用有效;其它过程是多次(multi-shot) 的。
多次过程调用时控制可能通过调用被再次转移,即嵌套调用(nested call) 。
一些被多次调用的过程可能被多次进入,即重入(reenter) 。
一个调用中的重入相同或不同过程的次数称为调用的深度(depth) 。
推论:嵌套调用是深度大于 1 的调用。
通过嵌套调用直接(总是以自身作为调用者)或间接(通过其它调用者转移控制)的重入是递归调用(recursive call) 。
过程可以返回(return) 取得计算的值并可同时改变控制状态,影响之后的计算。
原理
一次过程,特别是在其内部涉及和续延或闭包的实现交互时,相对多次过程可能具有因其对持有资源的要求较宽松,而具有较小的性能开销。
注释
对象语言中的过程在描述操作语义的元语言中可表示为函数,其应用可对应对象语言中过程的隐式调用。
违反一次过程调用有效地约束的程序典型地引起错误。
注意过程不一定可作为可被对象语言直接表达的一等(first-class) 函数,但同时在元语言中仍然可能可行。如无界续延,因为可能不符合函数的类型要求,详见续延的捕获和调用中的原理。
一次重入的过程调用分配的资源对应一个活动记录帧。
按计算的顺序约束和默认返回控制的方式,可能有不同的形式。
例程(routine) 的求值不交叉(interleave) ,即例程中的计算和例程外的计算非决定性有序。
注释 典型地,例程中的计算通过例程作为函数实体创建时的函数体确定。
作为不同的例程,不考虑例程中的计算的续延被保存时:
- 子例程(subroutine) 在返回一次后不重入。
- 协程(coroutine) 则可能被多次重入并引起多次返回。
和子例程的正常控制不同,即便其中的计算不涉及显式地改变控制状态,协程可能蕴含控制从协程中的计算到协程外的计算的转移:
- 引起多次返回对应改变控制作用。
- 转移控制后,函数体中的计算被暂停(suspended) 。
- 重入的协程可恢复(resume) 被暂停的计算。
- 不排除可被重入的协程作为函数实体,是可恢复函数(resumable function) 。
- 可被暂停和恢复的计算是异步的(asynchrnous) 。这和正常控制的同步的(synchronous) 的计算相对。
一般的续延支持返回多次并可能支持和调用者并发的计算,包括异步的计算;而协程蕴含的控制作用的改变对应不同续延的替换,也能实现类似的支持。
语言的语法可显式指定例程创建协程,也可以当前的控制状态创建和现有的例程没有直接对应的协程。后者类似续延捕获。
NPL 支持函数求值得到过程。对象语言中的过程可能支持使用这些形式的一种或多种,具体形式由派生实现指定。
协程可能限制转移向下一步骤的计算转移的方向,即调用者和被调用者被通过创建其的语法构造确定,而不能在之后改变。
根据是否只提供一种不区分转移方向的原语,协程分为对称(symmetric) 和非对称(asymmetric) 协程:
- 对称协程转移控制到另一个协程,不需要单独区分不同的操作。
- 非对称协程对控制的转移分为调用(invoke) 和出让(yield) 操作,其中:
- 一些协程称为半(semi) 异步协程(半协程),以体现实现异步计算的控制转移形势受限的非典型性。对应地,没有此类限制的协程被称为全(full) 异步协程(全协程)。
- 通常半协程指对控制的转移(相对传统的例程调用)受限,不能仅通过调用而需要单独的出让操作实现计算的暂停。这是非对称协程的同义词。
- 但半协程也可能指特指暂停在特定上下文受限的协程实现。
- 注释 另见这里的说明。
根据是否协程持有活动记录帧,协程分为有栈(stackful) 和无栈(stackless) 的。
因为具有类似的改变控制的能力,有栈的、可作为一等对象的全协程(full coroutine) 可替代一等续延。
原理
- 参见这里。
- 其中,对称协程类似一次无界续延,非对称协程类似一次有界续延。
- 对称协程可通过非对称协程补充操作实现。
- 注释 一个这种设计的例子参见 [WG21 P0913R0] 。
- 类似地,有界续延可通过添加显式的界限实现无界续延。
- 但是,这种功能相似不表示一一对应。
- 注释 参见以下关于出让操作使用续延实现的讨论。
- 对称协程可通过非对称协程补充操作实现。
- 在核心语言支持存储(store) 即可修改的一等状态的副作用的前提下,非对称协程和对称协程在可表达性上等价。
- 有界续延可不依赖其它副作用表达状态,但无界续延无法表达。
- 一等续延和协程在一定条件下可互相实现。
典型的设计中,函数表达式默认创建例程,而协程使用特设的语法标记过程得到。特设的关键字(如 yield
)提供语法,对应非对称协程中的出让操作。
注释
关于过程的参数和过程调用之间的计算顺序,参见求值策略。
λ 抽象(lambda abstraction) 是 λ 演算中的基本构成之一,提供匿名函数。
注释 λ 抽象的语法包含的形式是典型的操作符。
在原始的无类型 λ 演算中,λ 抽象不支持蕴含副作用,子表达式求值顺序任取而不改变范式的存在性和值。
Vau 抽象(vau abstraction) 是 vau 演算 ([Shu10]) 中的基本构成之一。
Vau 抽象创建的过程是操作合并子。
注释 使用 vau 抽象可实现引入 λ 抽象的操作符,如 [RnRK] 提供的 $vau
操作合并子。
具有操作符和操作数的项的组合可被特定的方式进行规约。这种组合是函数合并(function combination) ,包含:
- 具有至少一个约定位置的子项
E1
的复合表达式E
,当且仅当E1
是被求值作为操作符的函数时,E
是函数合并表达式(function combination expression) 。 - 其它具有操作符和操作数的项是非表达式形式的函数合并。以下操作符和操作数记作
E1
和E2
。
以下规则中,非表达式形式的函数合并也可被视为表达式求值。
求值函数合并时,子项 E1
总是被求值。
除 E1
外表达式的剩余子项 E2
是操作数,在 E 被求值时以操作数决定的值等效替换(substitute) 函数的形式参数。
替换形式参数的值是实际参数(actual argument, argument) 。
函数合并的求值是值替换规则的非平凡形式。
若替换操作数 E2
在合并中被求值,函数合并 E 是函数应用表达式,简称函数应用(function application) 。
若操作符是 λ 抽象,E2
视为一个整体,则函数应用替换规则对应 λ 演算的 β-规约(beta-reduction) 规则。
其它函数合并使用的替换规则由派生实现指定。
派生实现应指定函数合并规约的结果是规范形式,它对应的值是函数合并的求值结果替换被求值的表达式的实体,称为函数值(function value) 。
函数应用匹配实际参数和对应的引入形式参数的构造。匹配可能失败。确定匹配参数成功的条件是等价关系,称为参数匹配一致性,由参数匹配的等价关系指定。
匹配成功的每个实际参数和被匹配的目标(可能是形式参数)具有一对一或多对一的对应关系。
伴随参数匹配,实现可引入其它必要的操作(如为匹配分配资源和确定上述对应关系)。这些操作可具有和确定参数对应关系的匹配之间非决定性有序的副作用。
仅当上述必要操作及所有实际参数的匹配成功,替换 E1
决定的某个关联表达式中和形式参数结构一致的子表达式为实际参数。替换参数的结构一致性是等价关系。
表达式相等蕴含参数匹配一致性和替换结构一致性。实现可分别定义其它规则扩充这些等价关系的外延。
替换参数的值蕴含对实际参数的计算的依赖,即参数若被求值,其值计算先序函数应用的求值;但其它求值顺序没有保证。
注释
一般地,根据 E1
的值,操作数或操作数的值计算的结果被作为实际参数。
过程及其调用在其操作语义的元语言中通常表达为函数及函数合并。
若过程的结果被忽略,则通常表达为单元类型的值。
此外,一些语言中忽略过程的结果是空类型,以检查错误的使用。NPL 不要求语言具有静态类型规则,也不要求这些检查。
求值函数合并包含子表达式的求值:总是求值操作符,并可能求值操作数。若这些求值都没有退出,则发生函数调用(call) ,函数是被调函数(called function) 。
若被调函数存在形式参数,函数调用首先以操作数的直接子表达式作为实际参数,匹配实际参数和形式参数。
若实际参数匹配的目标可指定一个变量,则伴随参数匹配的操作包括以特定规则绑定的形式参数。
绑定的实际参数和对应的形式参数作为不同的实体时,作为伴随参数匹配的必要操作的一部分,发生参数传递(parameter passing) 。参数传递使形式参数具有作为实际参数值的副本。参数传递可能使和实际参数相关的资源被复制或转移。
实现在函数合并的求值中应提供函数调用的支持。
函数调用确定副作用的边界:保证参数表达式在函数应用被求值之前被求值。
在控制返回时,函数调用内部确定的值最终替换被求值的函数合并而作为函数值,即为返回值(return value) 。
若函数是过程,对应的函数调用是过程调用(procedure call) 。
若一个函数的调用仍待返回,则该函数调用是活动的(active) 。
调用总是不蕴含非纯求值的函数是纯函数(pure function) 。
函数调用的中蕴含的求值对应的规约步骤的集合是它的动态范围(dynamic extent) 。
函数中被捕获的实体的引用和求值函数中的计算创建的对象的引用构成函数计算的结果时,引用可能逃逸(escape) ,即在调用的动态范围以外可访问。
派生实现可能约定附加的名义特征区分其它情形相同的调用,称为调用约定(calling convention) 。
注释
典型实现的函数指称过程,函数调用是过程调用。
一般地,被调用的函数及函数调用的作用的等价性通常不能被确定。
一个重要的子类是不能确定具体表示的情形,参见合并子。其它函数一般也有类似限制。
关于函数调用中的求值,另见函数调用的终止保证。
和 [RnRS] 不同,动态范围仅对求值定义,而不是关于环境中的绑定显示计算作用的属性。这种属性事实上对象的生存期,仅对对象而非更一般的实体有效。
续延可用其动态范围表示。
本文档的动态范围的概念定义和 [RnRK] §7.1 的定义兼容,但不依赖其对续延的描述,也适用抽象机语义,是 [RnRK] 的一般化。
[Racket] 使用求值的规约步骤在表达式上定义动态范围。NPL 不在表达式上采用类似的定义,因为:
- 类似 [RnRK] ,NPL 强调支持对象语言中的显式求值风格及表达式求值前后的不同。
- 类似 [RnRK] ,进一步地,NPL 派生语言(如 NPLA1 )可明确支持在对象语言中指定求值环境而改变求值的上下文,表达式不能被预期通常以上下文无关的方式被求值。
调用约定可提升实现细节,为互操作提供接口保证,避免非预期的不兼容实现的混合。
除非另行指定,NPL 假定函数合并满足以下典型情形,即函数合并的操作符求值为以下类型的合并子(combiner) 之一:
- 对操作数的直接操作(而不要求对操作数求值)的合并子是操作合并子(operative combiner) ,简称操作子(operative) 。
- 进行函数应用的合并子是应用合并子(applicative combiner) ,简称应用子(applicative) 。
- 由派生实现定义的扩展合并子(extended combiner) 。
合并子的函数应用(依赖对操作数进行至少一次求值)是合并子应用(combiner application) 。
合并子应用使用应用序。
应用子总是对应一个底层(underlying) 合并子,可通过底层合并子上的一元的包装(wrap) 操作得到;其逆操作为解包装(unwrap) 。
解包装结果不是扩展合并子的合并子称为真合并子(proper combiner) 。
合并子上可以定义若干等价关系,这些等价关系蕴含关于函数应用替换的基本形式:
若对任意上下文,替换一个应用中的合并子为另一个不改变函数应用替换的结果,则这两个合并子等价(对应 λ 演算的 β-等价)。
注释
注释 详见续延的捕获和调用。
由于程序可能引入未知具体表示的合并子(如从其它模块链接),以上等价可能无法判定,不要求实现提供。
因为本设计不依赖 λ 抽象的内部表示(特别是支持惰性求值为目的的),不依赖 η-变换的可用性,也不要求支持更强的 βη-等价。
派生实现可按需定义较弱的等价谓词,保证其判定结果蕴含上述等价关系的结果。
语言可提供作为一等实体的续延即一等续延(first-class continuation) 。
续延的捕获(capture) 具现当前续延为对象语言中可操作的一等续延。
类似过程,续延可被一次或多次调用,称为续延调用(continuation call) 。
续延调用接受一个实际参数作为传递给后继规约步骤使用的值。除非另行指定,续延参数被按值传递。被调用的续延可访问参数并执行其蕴含的其余规约步骤。
和接受实际参数对应,续延可被假定关联一个等效的应用子,具有一个形式参数,这个应用子的底层合并子被调用时非正常地传递它的操作数给关联的续延。
对象语言可支持符合函数类型要求的一等续延作为函数。作为一等续延的函数可直接作为合并子构成函数合并进行函数调用,而实现续延调用。
除非派生实现另行指定,NPL 的一等续延不是函数。
续延调用的其它的具体形式由派生实现定义。
除非在捕获的续延上存在特定的控制作用,合并子被调用时以当前续延返回且仅返回一次。
类似函数应用表达式,续延应用(continuation application) 表达式是求值时蕴含续延调用的表达式。
原理
在 Scheme 中,一等续延即过程。
在限制元语言的函数不蕴含控制作用时,类似 Scheme 等支持的无界续延不是函数。一个理由不能以常规方式为无界续延指定是函数类型。参见这里的介绍。
在 Kernel 和其它一些语言中,续延不是过程,而具有不同的名义类型。这种不同于 Scheme 的设计是有意的。
NPL 一等续延不限制是否和函数类型同一,因此无界续延仍可被视为函数(或更确切地,即程序入口作为边界的有界续延)。
类似 [RnRK] 的设计,因为一等续延的调用可引起和更常见的过程调用显著不同的控制作用,续延调用有必要和过程调用在对象语言的语法上显式区分以满足易预测性,因此一等续延一般不是函数。
续延关联的等效应用子的原理同 [RnRK] §7 和 §7.2.5(应用子 continuation->applicative
)的原理,但略有不同:
- 作为一等对象的续延和续延的实际参数是否求值无关,因此不是合并子,求值算法不需要支持续延作为函数合并被求值;但续延可通过特定的操作转换为应用子。
- 续延和操作子在被调用时都接受一个实际参数对象。
- 但是,为避免和简单性冲突,[RnRK] 的选择器(selector) 支持在此未被要求。
注释
类似过程,续延及其调用在其操作语义的元语言中能表示为元语言的函数应用,通常表达为函数及函数合并。
续延捕获在语法上类似函数对变量的捕获。被捕获的实体通常以引用保存。被捕获的实体通常是隐式的,即不在对象语言程序中出现。
在支持一等续延且捕获的续延可被复制的语言中,实现需要考虑活动记录的复制,参见 [Hi90] 。
关于控制作用,另见续延调用对程序控制的改变。
活动的合并子分配的对象称为活动记录(activation record) 。
函数调用以活动记录引用涉及的变量。每个调用的活动记录中可保存多个变量。活动记录可能因此持有状态,即便不一定可被函数调用外的操作直接修改。
嵌套的函数调用具有多次分配的活动记录。为强调其中的对应关系,每一个调用关联其中的一个帧(frame) 。
在确定一次分配的一个活动记录对应一次函数调用的实现中,一个活动记录和一个活动记录的帧同义。
活动记录的集合可能构成特定的数据结构。例如限制只支持嵌套的子例程调用(而不支持一般的续延调用)时,具有后入先出(LIFO, last-in-first-out) 的栈的结构。
在变量绑定值后,兼容 λ 演算规约语义(特别地,β-规约)的表达式的具体求值根据是否传递操作数对使用按需传递的情形分为三类:
- (完全)惰性求值(lazy evaluation)
- 部分惰性求值
- 热情求值(eager evaluation)
其中,惰性求值总是使用按需传递,热情求值总是不使用按需传递,部分惰性求值不总是使用或不适用按需传递。
在保证不存在非纯求值时这些求值的计算作用没有实质差异。存在非纯求值时,使用的 λ 求值策略由派生实现定义。
非严格求值严格蕴含惰性求值。两者经常但不总是一致,例如,实现可能并行地热情求值,并舍弃部分结果以实现非严格求值。
热情求值蕴含严格求值。两者也经常但不总是一致,例如,实现可能使用应用序严格求值。但因为非严格的热情求值缺乏性能等可局部优化的实用动机,这种不一致的情况通常不作为附加的语言特性提供(而仅为简化实现默认作为全局策略使用)。
注释
由于实现可能确定特定表达式的作用对约定必须保持的程序行为没有影响而可能省略求值,按抽象机语义的严格求值在实际实现中通常是不必要的。
惰性求值可通过中间值延迟求值实现。
上下文(context) 是表达式关联的状态的特定集合。
注释 这里不是自指概念。
一个上下文是显式的(explicit) ,当且仅当它可以通过名称表达式访问。
一个上下文是隐式的(implicit) ,当且仅当它不是显式的。
隐式的上下文通常是管理状态。
规约规则中,以未指定子项参数化的项是一个上下文。
本节以外其它关于上下文的具体规则由派生实现定义。
注释
参数化的子项可在(元语言的)语法上被表示为一个洞(hole) ,详见上下文相关求值中的语法 []
。
过程实体能影响函数表达式关联的上下文,参见函数和函数应用的求值环境。
求值环境(evaluation environment) 是在求值时可访问的隐式上下文,提供可通过名称解析访问的变量的绑定。
不和实现环境相混淆的情况下,求值环境简称(变量或对应的局部绑定所在的)为环境(environment) 。
具有可见名称的绑定是可见的(visible) 。
环境包含(contain) 若干个局部绑定(local binding) ,即不通过其它环境即保证可见的被绑定实体(bound entity) 。
环境展示(exhibit) 可见的绑定。
一个环境是空环境(empty environment) ,当且仅当其中包含的局部绑定集合是空集。
注释
按绑定的定义,求值环境的局部绑定集合即变量的名称和通过声明引入的被变量表示的实体构成的映射。
可见绑定可能被通过名称解析成功访问变量。
包含和展示的定义同 [RnRK] 。除此之外,环境对象具有直接包含的绑定的所有权。
实现环境可能在实现以外提供附加的求值环境作为任务通信的机制,如环境变量。
除非派生实现另行指定,语言支持的求值环境和这些机制蕴含的求值环境的交集为空。语言可以库的形式提供 API 另行支持。
在典型的对象语言中 λ 抽象中指定的替换构造具有局部作用域(local scoping) ,其中可访问 λ 抽象外部词法意义上包含的(enclosing) 求值环境的变量,对应求值环境为局部环境(local environment) 。
在基于词法作用域(lexical scoping) 的对象语言中,引入 λ 抽象对应的语言构造支持捕获引入函数时所在的作用域的环境,称为静态环境(static environment) 。
相对地,动态作用域(dynamic scoping) 根据求值时的状态指定指称。
Vau 抽象进一步支持在局部环境中提供访问函数应用时的求值环境,即动态环境(dynamic environment) 的机制。
除非另行指定,按词法闭包(lexical closure) 规则捕获,即只根据词法作用域确定捕获的指称;若需要支持依赖求值状态动态确定指称时,使用派生实现提供的对求值环境的操作,而不依赖动态作用域。
作为过程的实现,词法闭包规则捕获实体创建闭包(closure) 。
除非另行指定,NPL 只存在一种作用域,即所有作用域都使用相同的名称解析和捕获规则。
注释
历史上,闭包首先在 SECD 抽象机中引入。术语闭包来自 λ 演算的闭项。
用于互操作的和求值关联的隐式上下文是互操作上下文(interoperation context) 。
除非派生实现另行指定,语言不提供访问互操作上下文的公开接口。
注释
一个典型的实例:由 ISA约定的通用架构寄存器的状态,可能需要在函数调用或任务切换过程中保存和重置。
类型(type) 是上下文中和特定的实体直接关联或间接关联的元素,满足某个执行阶段的不变量约束。
类型规则(type rule) 是和类型相关的对象语言的语义规则。
实体关联的类型可能被显式地指定,或通过隐式的限定规则推断确定。符合指定和限定要求的类型可有任意多个。
实体的类型是被显式指定的实体关联的类型。实体具有实体的类型以及通过其它规则限定的类型。实体是类型的实例(instance) 。
类型可用集合表示。集合的元素是具有其表示的类型的实体。
表示类型的集合为空时,表示类型没有实例,是空类型(empty type) 。
推论:由集合的形式表达,空类型是唯一的。
表示类型的集合只有一个元素时,类型只有一个不可区分的实例,这样的类型是单元类型(unit type) 。
和表达式直接关联的类型满足起始阶段不变量约束,称为静态类型(static type) 。
和表达式的值关联的类型满足运行阶段的不变量约束,称为动态类型(dynamic type) 。
其它可能存在类型或实现执行阶段的扩展由派生实现定义。
除非另行指定,对象的类型是对象的值的类型。
NPL 对象类型和存储的值的类型之间的关联未指定。
类型在描述类型规则的元语言中可作为对象。
生成对象的元语言函数是类型构造器(type constructor) 。类型构造器的参数是类型,的函数值是组合这些参数得到的复合类型(compound type) 。
称为类型的具体实体和之间的关联由派生实现的类型系统(type system) 规则指定。
默认类型系统不附加约束,所有表达式或关联的项都没有指定类型(untyped) ,为退化的平凡类型系统(trivial type system) 或单一类型系统(unityped system) ,实质上是动态类型。
对类型系统的分类中,类型也指确定类型的过程称为类型机制(typing discipline) ,其中确定类型的过程称为定型(typing) 。
在静态类型之后阶段确定的类型机制是动态定型(dynamic typing) 。
除非另行指定,被确定的静态类型的阶段是翻译时阶段;被确定的动态类型的阶段是翻译时之后,即运行时。
语言可提供定型规则(typing rule) (en-US) ,指定项作为实体在特定的上下文(称为类型环境(typing environment) )中的类型。项是类型在这个上下文中的居留(inhabitant) 。
类型环境确定类型指派(type assignment) ,即项和类型的之间的定型关系(typing relation) 。定型确定的这种定型关系的实例即定型判断(typing judgement) 。
不违反类型系统规则下的良定义的程序构造是良型的(well-typed) 。
根据是否要求项首先都是良型的再指派语义,带有类型的形式系统可具有内在(intrinsic) 和外在(extrinsic) 的解释。
除非另行指定,NPL 使用外在的解释。
原理
默认使用外在解释的理由是:
- 类型的外在解释允许在一个没有指定具体类型系统设计的单一类型系统为基础扩展不同的类型系统,能满足语言自身可扩展的需要。
- 扩展通用目的语言特性的顺序应是从简单到复杂的,而不是相反,因为并不存在已知的万能语言可供裁剪。
- 这也符合历史顺序:无类型 λ 演算被扩展到不同的有类型 λ 演算,而不是相反;因为有类型 λ 演算的规则明显较无类型 λ 演算多且复杂。
- 从无类型 λ 演算可以扩展到的一些特性更丰富其它系统,如 λμ 演算 (en-US) 和 vau 演算,首先都是无类型的,并不存在可用的内在解释。
- 为了描述类型规则,外在解释最终需要在整个系统中引入和对象语言不同的元语言,而增加复杂性。
- 即便存在强调可扩展的对象语言(如 MLPolyR),至少语言规范中定义的元语言没有被证明可以和被描述类型规则的对象语言合并。
- 即便能证明可以合并,这种方式也显著地大大增加了设计的复杂性,违反避免不必要付出的代价。
- 根本上,这种方式损害对象语言设计的光滑性,很可能大大削弱对象语言的可用性。
- 没有确切的充分依据证明引入类型系统带来的性质是通过非类型论的直接扩展演绎系统的方式不能实现或者其实现有现实困难的。
- 因此先验地要求类型的存在缺乏必要性。即便可实现需求,在通用目的上通常是舍近求远的过度设计。
- 即便引入类型的方式有现成的工程实践而可以提升工程效率,也可能是过早的优化。
- 更何况现实并没有证据表明存在这样的成功实践。
- 跳出先验地引入类型的做法,使用先验的内在解释而排除不够清晰明确的含义(meaning) 的语法的方式,在历史上存在更显著的失败。
- [Chu41] §18 试图排除原始的 λ 演算(称为 λ-K-转换,在 [Bare84][Shu10] 中称为 λK 演算)中无法取得范式的项(以使之更适用于符号逻辑的目的):限制 λ 抽象中的约束变量是第二子项的自由变量。
- [Bare84] §2.2 指出 λI 演算具有的一些问题,如:
- 对应的理论 λI 翻译到组合子逻辑的理论 CL 时,项能取得范式的性质不被保持。
- 范式的概念过于侧重语法,所以在模型中不确定含义。
- 试图识别编码偏函数(partial function) (en-US) 需要的“未定义”的项是不可能的。
- λI 演算定义的偏函数的组合对应的项不一定是 λI 演算定义的被组合的偏函数的项的组合。
- [Bare84] §2.2 指出,这些问题都来自 [Chu41] 选择用无法取得范式的项编码“未定义”的概念。
- 即便不考虑上述整体性质,尽管计算上 λI 演算是 Turing 完备的,它不能编码常量函数。
- 以上问题一定程度上揭示了去除似乎冗余但实际在语义上可能非平凡的语法构造是不成熟的简化,损害系统的可用性。
- 要求(可被类型检查的)类型系统直接排除不能取得范式的项,在这个意义上比 λI 演算对去除特定的项的组合更彻底。
- 即便类型系统能引入其它语义,这以引入不能被对象语言表达的规则为代价,通常需要元语言。
- 相比之下,同样是引入对象语言表达式无法表达的语义,管理规约是对象语言规则能直接蕴含的,相对具有更小的(工作量和避免兼容问题上的)代价。
- 注释 内在解释又被称为 Church 风格的。
- 哲学意义上,内在解释或本体论上的(ontological) 解释,相比外在解释或语义上的(semantical) 解释需要更强的假设。
- 本体论上的逻辑,如 Frege-Church 本体论 (en-US),可能解决一些悖论。
- 但根本上,没有充分动机指出,不涉及演绎规则的悖论必须在通用语言内部直接提供规则消除,而不能通过其它方式(例如,由用户程序补充前提)解决。
- 指定管理规约可以编码非平凡的表达语言规则外的语义的项可以对这样的前提建模并在语言中适当编码表达。
- 编码表达这种方式是 NPL 强调 N(name) 和其它实体分离的主要理由。
- 本体论假设要求名称以外附加实体以使假设生效。一般地,这些假设以不同语言的陈述作为断言实现。这些陈述涉及特称对象时,在完备性上是可疑的,且容易和开放世界假定冲突。
- 约定不涉及的语言规则的本体论假设在这些意义上也可被认为在效用上的不成熟的优化。
注释
在元语言的意义上,类型系统包含语法和对应的语义,但在对象语言中,定型规则和其它推理规则(如类型检查规则)作为语言规则是语义规则,和语法相对独立。
实体的类型可被指定为未指定类型,以明确类型的存在性,但不明确具体的类型的构造和表示。
形式地,在类型系统中,类型环境和项作为前提,通过定型规则(typing rule) 得到定型判断。定型规则在逻辑上可以是公理或定理。
在数理逻辑中,使用结构主义数学方法,集合可以作为描述类型规则的理论(句子集合)的模型,和理论支持描述的类型一一对应。
通过显式指定标识(如名称)的方式定义类型的方法是名义类型(nominal typing) ,否则是结构化类型(structrual typing) 。
除非另行指定,不同的名义类型不蕴含等价关系。结构化类型之间的等价关系由实现定义。
类型的相等关系是一种类型之间的等价关系。两个类型相等,当且仅当它们的实例作为元素的两个集合对应相等。
除非另行指定,相等的类型不在语言中区分,且元语言(描述对象语言的规则)中类型作为实体的同一性即类型相等性。
推论:除非另行指定,不同的类型不等价。
除非另行指定,对象语言使用类型相等性实现类型等价性。
原理
对象语言中的类型实质上是类型的一种间接的表示,作为实体仍然可以具有不同的同一性。
这避免程序可能需要枚举类型的外延(即精确实现出表示它的集合)才能确保确切表示出这个类型这样的计算上不可行的困难。
因为可支持的表示的类型全集不同,类型相等是相对的,依赖类型系统的具体实现。一个类型系统可能支持无法在另一个类型系统中精确表示的类型。
注释
本节的主要例外参见公共子类型。
根据是否需要特定的文法元素指定和项关联的类型即类型标注(type annotation) ,对确定类型的机制可进行分类。
类型系统可使用显式类型(explicit typing) ,即在定型时要求类型标注。
不使用类型标注的方式是隐式类型(implicit typing) 。
在引入实体(特别地,如变量)时指定实体的显式类型标注称为清单类型(manifest typing) 。
不使用清单类型而使隐式引入的实体(如值)关联具体类型的机制称为潜在类型(latent typing) 。
清单类型是显式类型的实例;除此之外,显式类型还包括铸型(casting) ,即显式指定表达式求值的结果应具有的类型。
潜在类型是隐式类型的实例;除此之外,隐式类型还包括类型推断(type interferece) ,即通过隐含的上下文信息判断表达式关联的类型。
类型推断的逆过程是类型擦除(type erasure) 。类型擦除支持使一个良型的程序中的已被定型的实体表示擦除前按类型规则不允许表示的其它实体。
若类型机制可保证在某个执行阶段内有确定强规范化性质的算法确定类型,则类型机制在该阶段是静态定型。
注释 强规范化性质的算法保证终止。
语言可能个别指定引入这些类型相关的规则,在保持逻辑相容的前提下可混合使用。
显式类型可编码接口的要求,即类型签名(type signature) 。
类型签名通常直接指定名义类型,但同时也可允许非特定的满足结构类型约束的类型。这些类型和类型签名兼容(compatible) 。
原理
历史上,表达式的类型和变量的类型在简单类型 λ 演算中同时被引入。后者修饰 λ 抽象中的自由变量,而前者限定剩余的所有项。
即便从项重写系统中两者是形式上统一的,在实际语用中具有很不同的差异。这集中体现在后者是名义的,除非附加其它不同的语法设施,并不具有结构化推导的性质,原则上只适合描述接口;而前者能兼容结构化类型,同时适合描述接口及其实现。
作为接口的名义类型在作为自由变量以外的上下文中重新复用为不关心其类型(并消除依赖这些信息的其它机制)的其它程序构造(一般意义上的表达式),通常需要类型擦除等更复杂的机制和支持的类型系统规则,以消去不再预期和其它类型系统规则交互的类型。
和 [RnRK] 类似,NPL 不要求使用清单类型,以避免一些一般意义上的全局设计缺陷。这些缺陷包括:
- 过于积极地(非预期地)排除危险但对程序有用的使用,而违反易预测性。
- 因为移除类型标注需要上述的复杂机制和类型系统规则,具体的清单类型阻碍派生语言定义其它不容易冲突的类型标注规则而使语言具有更好的可扩展性。
- 因为名义类型的相关规则更容易直接拒绝一些和类型规则不兼容的程序构造而难以简单地变通,往往对程序构造的组合具有更多直接的可表达性限制而破坏通用计算意义上的正确性。
若有必要,派生语言仍可限定使用清单类型。一般仍然建议仅在局部引入而避免全局复杂性和因此带来的限制。
注释
类型签名来自数理逻辑术语。
类型检查(typechecking) 解答程序是否满足类型规则的判定性问题。
使用翻译时的语义分析或运行时的类型检查分别为静态类型检查和动态类型检查。
静态类型检查规则是可诊断语义规则。
语言可能个别指定引入类型检查相关的规则,在保持逻辑相容的前提下可混合使用。
类型检查失败引起的错误称为类型错误(type error) 。
注释
注意静态类型检查和静态定型以及动态类型检查和动态定型的区别。类型检查和类型机制是不同的规则,不必然包含蕴含关系。
类型检查的一个典型的使用场景是类型签名的兼容性校验。
类型全集(type universe) 是语言规则中允许表达的类型的总称。
注释 表达类型的规则构成的模型的语言是语言规则的子集。
NPL 避免限定类型全集。派生语言可指定不同的规则。
除非派生实现另行指定,程序的用户不能依赖语言规则的限定枚举类型全集中的所有类型。
原理
判断值是否满足类型居留(inhabitant) 的谓词是类型谓词(type predicate) 。
注释
和 [RnRK] 的基本类型谓词不同,类型谓词定义为只接受一个参数。
类型之间可具有序关系。
被定型的类型的实体可完全地满足其它类型的约束。前者具有后者的子类型(subtype) 。
子类型(subtyping) 关系是一种预序(preorder) 关系,即自反的、反对称的二元关系。
相等的类型符合子类型关系,是平凡的(trivial) 。排除平凡的子类型关系是严格子类型关系。
严格子类型是严格预序关系,即反自反、反对称的二元关系。
子类型和严格子类型对应的逆关系是超类型(super typing) 和严格超类型(strict supertyping) 关系。
多个类型可具有公共的(严格)超类型。这些类型同为一个类型的子类型而等价。
除非另行指定,在程序的行为不依赖其中特定的个别不相等的类型而具有差异时,具有相等超类型的等价的子类型视为相同的类型。
复合类型中其中一部分的类型替换为其子类型,得到的结果和原复合类型可能有如下变化(variance) 的对应关系之一:
- 协变(covariant) :类型序被保持,即结果类型是原复合类型的子类型。
- 逆变(contravariant) :类型序的逆被保持,即结果类型是原复合类型的超类型。
- 不变(invariant) :不保持类型序,即结果类型和原复合类型之间没有确定的子类型关系。
同时存在以下派生归类:
- 互变(bivariant) :同时协变和逆变。
- 可变(variant) :至少协变或逆变之一。
对接受参数类型得到结果类型的函数类型构造器 → ,以下关系是确定的:
- 参数类型对函数类型逆变。
- 结果类型对函数类型协变。
把LSP要求子类型经替换前后保持性质的谓词视为类型构造器,则 LSP 要求的性质是协变的。
注释
关于 → 的变化关系的陈述通常直接被作为类型系统中的定型规则表达的公理,以和 → 既有的定型规则兼容。
一些非普遍的局部类型序的构造器,如数组的下标 []
,也可对参数有确定的可变关系。
在 LSP 的原始论文提供了两个满足 LSP(文中称为子类型要求(subtype requirement) )的在过程的签名定义子类型的方法,兼容以上传统的函数类型构造器的子类型变化关系,是 → 上的上述关系的扩展:这些定义还支持表达过程的具体前置条件(precondition) 和其中引发的异常。
对一般的谓词,LSP 的行为多态(behavioral polymorphism) 是不可判定的。因此,一般的 LSP 无法被类型检查。在类型系统中应用 LSP 需依赖具体能表达性质的谓词,如使用的类型构造器。
一个类型系统可指定唯一的底类型(bottom type) (en-US) 作为其它任何不同类型的严格子类型,记作⊥。若类型全集包含空类型,则底类型是空类型(empty type) (en-US) 。
一个类型系统可指定唯一的顶类型(top type) (en-US) 作为其它任何不同类型的严格超类型,记作⊤。这种类型即通用类型(universal type) 。
NPL 支持空类型作为底类型,但不要求在对象语言中支持其表示。
NPL 避免要求唯一的顶类型的存在以符合开放世界假设。
派生语言可指定不同的规则。
原理
以空类型作为子类型在类型序的推理上是自然的。
就非特定的类型全集,通用类型的的构造和表示不唯一,因此不能直接断言其存在。
否则,假定存在这种类型,则断言不存在其超类型,这可能和其它语义规则冲突。
即使在名义上定义具体的超类型(如 Java 的 java.lang.Object
),也面临不能向上扩展(得到比 Object
更基本的类型)的问题,违反最小接口原则和通用性。
具体的顶类型在断言当前类型系统不存在公共超类型可能仍然有实用意义;此时,顶类型即一等实体构成的类型,而不需要定义具体名义类型。
特定的类型系统支持类型签名能对应多种不同的兼容类型。这样的类型是多态的(polymorphic) 。
一般地,类型上的多态(polymorphism) 有:
- 特设(ad-hoc) 多态:仅对项上局部的项上的类型作用使之满足上下文兼容要求的多态:
- 参数(parameteric) 多态:接口签名指定以具体类型作为值的变量,组合为函数或者其它接口对应实体的类型。
- 子类型多态:接口签名编码接受子类型关系作为兼容类型。
- 行(row) 多态:对组成具有名称和实体对构成的元素作为成员(member) 的实体,兼容限定部分成员的类型。
多型(polytipic) 的接口在同一个接口签名上以结构化类型的隐式类型构造支持不同的类型而支持多态。
注释
重载在一些语言中自动地对函数对应的具体可调用实体适用。
行多态以结构化类型约束取代通常通过名义类型指定的子类型关系。
种类(kind) 是静态类型系统的语法表示中具有特定类型模式(pattern) 的分类。
一定意义上,种类是类型系统的元语言中一种元静态类型。
一般地,实体类型的种类记作 *
。
除非另行指定,作为项的函数应具有函数类型,即符合类型种类为 * → *
的结果的类型,如为简单类型 λ 演算兼容的函数类型实例。
其中,→
是函数类型的类型构造器。
种类作为元语言中的类型多态,实现种类多态(kind polymorphism) :接口签名接受类型的编码中对应位置具有不同种类的类型。
注释
在实现中,种类也被作为互操作的归类,如视为函数调用的调用约定。
但这不足以涵盖一般的形式定义;特别地,调用是仅仅关于过程这类实体的互操作,而种类适合一般实体的静态类型。例如,在不考虑进一步地实现时,多变(levity) 多态的类型不需要限定过程(函数)。
类型系统中的种类也可扩展到特定的计算作用的作用系统(effect system) 上以描述作用的种类,此处从略。
一等对象的类型是一等类型(first-class type) 。
非一等类型的居留可能不在对象语言中可表达,即对象语言中无法构造这些类型的值。
非一等类型仅用于构造其它类型(可能是一等类型)和类型检查等依赖类型的推理。
注释
一个典型的非一等类型的例子是 [ISO C] 和 [ISO C++] 等语言支持的类型 void
。
在语义的角度上,void
可视为依赖翻译阶段把求值时得到的对应 void
居留的表示替换为表示语义错误的单元类型,并在翻译结束前拒绝接受带有这种居留的程序,而这种居留在对象语言中始终不可取得。
若不限制翻译阶段,可通过在传递时始终限制正常控制的值实现类似的效果,例如不考虑类型消除时 [ISO C++] 中在复制或转移构造函数始终抛出异常的类类型。
程序的执行可被控制作用影响。蕴含这些影响的条件即执行条件(execution condition) 。
注释 这和过程的调用类似。
更一般地,规约规则指定语言的实现决定程序行为时使用的(对程序不保证可见的)续延,这种在实现中对应的控制状态称为控制执行条件。
和控制状态不同,控制执行条件描述语言提供的不同控制机制的分类,而不被作为语言可编程的特性提供。
除非另行指定,仅由求值算法中蕴含的规约规则决定的执行条件是正常(normal) 的。
合并子调用以当前续延返回是正常执行的。
注释 这是正常控制执行条件的一个主要实例。
改变程序的正常的控制要求存在控制作用,此时,控制执行条件是非正常(abnormal) 的。
除非另行指定,隐含在求值算法中蕴含的规约规则确定的函数应用外的续延调用是非正常的。
注释 这是非正常控制执行条件的一个主要实例。
具有规约语义的语言总是支持正常控制条件。NPL 中,非正常的控制条件的支持是可选的。
由派生实现定义的非正常的控制条件是异常(exceptional) 条件。
异常(excpetion) 是通过抛出(throw) 实体(称为异常实体)同时表达满足异常条件的控制作用的语言构造。
语言的实现或用户通过特定操作(如求值一个表达式)指定程序满足异常条件,使程序的控制进入异常执行状态,允许程序具有正常条件下可分辨不同行为。
程序通过捕获(catch) 并处理(handle) 被抛出的实体,程序可满足不同的恢复正常执行的条件。
进入违反翻译时正确性规则的异常执行状态时,由语言实现提供的异常执行机制实现行为。
注释 这些行为至少蕴含满足翻译时正确性规则要求的诊断。
进入其它异常执行状态的异常条件包括所有运行时异常条件和直接引起程序异常的用户操作。
这些异常条件的具体行为和正常条件下的不同由派生实现指定的运行时状态或直接引起异常(改变程序的控制)或语言构造的语义决定。此时,由实现定义使用的异常执行机制。
注释 其它异常条件的异常执行机制可能和上述相同或不同。
派生语言实现可指定以下规则:
- 符合以上约定的判断改变(进入和退出)异常执行状态的执行机制。
- 包括抛出和捕获的语言构造和其它可选的引起改变异常条件的上下文。
若派生实现不指定以上要求的执行机制和上下文,则不支持异常。
除非派生实现另行指定,异常的控制作用总是被同步(synchronized) 的,即:
- 在初始化异常实体时,保证存在与异常条件关联且可确定单一的执行线程的状态作为引起控制状态改变即引发异常的来源。
- 异常条件的满足不依赖未和引发异常状态同步的程序中的其它的执行状态(包括其它未同步的线程的状态)。
- 确认满足异常条件和进入异常执行状态之间,上述执行线程内程序仅在引发异常的线程上的程序允许存在计算作用(这保证不被引起可观察行为改变的其它线程的操作中断)。
除非派生实现另行指定,未捕获的异常总是确定性地(deterministically) 持续引发异常的执行线程中引起控制的转移:
- 若捕获操作有效的上下文,控制转移捕获构造处理对应异常的异常处理器(exception handler) 。
- 否则,若在活动函数调用中,则单向地从当前活动的函数向其主调函数转移控制,使后者活动。
- 否则,若没有找到剩余的活动函数调用,则程序异常终止。
除非派生实现另行指定,上述转移活动函数若成功(包括异常在活动的主调函数嵌套的特定语言构造中被捕获),先前不再活动的活动记录中的资源在控制成功转移后应立即被释放。
典型的设计中,求值规则使的正常状态的函数调用要求的活动记录分配和释放满足 FIFO(Last-In First-Out ,后入先出)的顺序,构成了栈(stack) ,活动记录是栈帧(stack frame) 。
除非派生实现另行指定,活动函数的转移释放资源,应保证按和创建被其所有的实体的顺序的相反顺序一致的形式释放。这种释放活动记录占用资源的机制称为栈展开(stack unwinding) 。
特定的求值具有(确定性地)终止(termination) 保证,当且仅当预期求值总是在有限计算步骤内可描述的计算作用。
具有终止保证的求值总是取得值或通过非正常控制的计算作用退出求值。
不具有终止保证的求值可能不终止,此时它具有取得值以外的计算作用;这种计算作用是副作用。
若一个函数的调用总是具有终止保证,则此函数是终止函数(terminating function) 。
若一个函数的调用总是取得值,则此函数是全函数(total function) 。
注释 全函数总是终止函数。
当前维护的主要派生语言为 NPLA ,是 NPL 的抽象语言实现和派生实现。
NPLA 的参照实现 NPLA1 是具体语言实现,约定特定于当前参照实现的附加规则和实现。
作为原型设计,NPLA 重视可扩展性。
作为 NPL 的派生实现,NPLA 对象语言的设计遵循 NPL 符合性规则,并满足如下要求或附加限制。
注释
NPLA1 是 NPLA 的一个派生实现。
- 位(bit) :表示二进制存储的最小单位,具有 0 和 1 两种状态。
- 字节(byte) :基本字符集中一个字符需要的最少的存储空间,是若干位的有序集合。
- 八元组(octet) :8 个位的有序集合。
NPLA 使用宿主语言为 [ISO C++11](及其之后的向前兼容的版本)的简单实现模型 NPL-EMA 。
以下要求和宿主环境一致:
NPLA 实体的内部表示是宿主语言中可表达的数据结构。
NPLA 实体的外部表示是宿主语言中可通过输入/输出操作处理的数据。
除非另行指定,NPLA 使用宿主语言提供的异常作为异常执行机制。
除非另行指定,程序不使用使宿主语言区域指定的行为(locale-specific behavior) 改变的特性。
原理
默认避免改变区域指定行为简化设计约定。
注释
关于类似的对宿主语言程序的要求,另见 YSLib 项目文档 doc/LanguageConvention.txt
。
NPLA 支持数值(numerical value) ,但不要求支持具体的数值计算。
NPLA 实现为派生实现提供数值类型和相关的操作的基本支持。
除非另行指定,若派生实现支持数值计算,其实现兼容 NPLA 数学功能的实现。
词法分析可接受多字节文本编码的字符串形式的源代码,但不假设其编码中除 0(空字符 NUL )以外的具体代码点被编码的数值,不转换编码。
使用可选的语法预处理和 NPL-GA 语法。
- 在构成 NPL 标识符的词素中插入有限个非 NPL 分隔符的字符且不构成 NPLA 扩展字面量的词素。
- 全由
+
或-
构成的词素。
NPLA 扩展字面量包括:
- 以
#
、+
或-
起始的但不全是+
或-
构成的、长度大于 1 的词素。 - 十进制数字字符起始的词素(当被支持时)。
全由十进制数字字符的词素表示十进制数值。派生实现可定义其它作为数值的词素。这些词素作为字面量时,是数值字面量(numerical literal) 。
名称仅被实现为和字符串的值的一个真子集一一对应的表示(参见类型映射)。
除非派生实现另行指定,只有代码字面量不是自求值表达式,其余字面量都求值为右值。
代码字面量求值时解释为名称。
数据字面量是自求值的字符串的外部表示。
数值字面量是自求值的数值的外部表示。
存在不保证先求值的子表达式的语法形式是特殊形式(special form) 。
特定的名称是保留名称(reserved name) 。
除非另行指定,在源代码中使用保留名称作为实体的名称的程序行为未定义。
名称解析失败可被忽略而不终止实现演绎;保证名称表达式求值的强规范化。
不要求提供命名空间实现的可变实体。
不保证求值都是纯求值;非特殊形式使用热情求值;其它情形使用热情求值或惰性求值的方式由具体特殊形式约定。
对象语言的函数默认为过程,过程默认实现为子例程。过程指定的计算结果和函数表达式最终求值结果的关联是过程调用的结果的恒等映射。
注释 即过程调用的结果总是同函数值。
除非另行指定,实现函数的宿主数据结构生存期要求默认同宿主语言。
原理
NPLA 函数不支持类似 [ISO C++] 的类型退化(decay) 。作为动态类型语言,需要被转换的值在操作内部实现,不需要在返回值上另行附加转换。
按值传递的复制初始化和宿主语言的对应语义类似。
NPLA 使用潜在类型:值具有类型;不指定动态类型以外的类型。
显式类型(如清单类型)的机制可由派生实现指定可选地引入。用户程序也可能添加类型标注和不同的类型机制的支持。
NPLA 使用和宿主语言相容的动态类型检查。除非派生实现另行指定或类型映射的需要,使用的类型检查规则和宿主语言一致。
宿主语言对象的值描述状态,且宿主语言要求的对 volatile
左值的操作属于可观察行为。
NPLA 的宿主语言应能提供 NPLA 及派生实现的本机实现。
NPLA 的派生实现提供特定的和宿主语言的互操作支持,可其中和 NPLA 提供的关于互操作的具体行为不同的部分应由实现定义。
注释 对派生实现,NPLA 约定的具体默认互操作特性是可选的。但是,一般的约定如开放类型系统仍被要求。
NPLA 和派生实现可约定互操作的具体实现的要求,以确保实现的状态可预测。
本机实现可以具有 C++ 的实现兼容的二进制接口的函数提供,这些函数称为本机函数(native function) 。
本机实现可直接支持本机函数在实现中被调用。若被支持,具体接口由派生实现指定。
本机函数作为函数的实现,其调用的求值可具有和非本机的函数一致的作用,但不需要具有可被对象语言表达的函数体。
为确保函数求值的作用可能保持一致,本机函数应符合和本机函数调用时使用的规约一致的方式使用,即在宿主语言的意义上至少符合以下规约调用约定:
本机函数的返回值应能表达任意的非本机函数调用的返回值,即通过求值函数调用中函数体的非本机函数的求值结果。
原理
实体的内部表示和外部表示满足实现环境的要求允许在宿主语言程序中直接实现关于表示的操作,简化了互操作机制的设计和实现。
注释
宿主语言自身的调用约定(通常和实现的 ISA 相关)作为 C++ 实现自身的 ABI ,在此是中立的,没有提供特设的支持的要求。
另见 NPLA 基础存储和对象模型。
类型映射(type mapping) 指定对象语言和宿主语言之间的实体类型之间的关系,是前者中的类型到后者中的类型的映射。
作为类型映射目标的宿主语言类型或其子类型称为宿主类型(hosted type) 。
作为宿主语言类型的宿主类型是典型的。其它宿主类型是非典型的。
具有特定动态类型的对象语言的值在宿主语言具有宿主类型,以宿主语言的值表示,称为宿主值(hosted value) 。
在互操作的意义上,宿主值在作为对象语言的值的表示的项中以宿主对象(hosted object) 的形式被保存并可在宿主语言中访问。
对象语言的值被对象语言的实体类型表示蕴含它被映射的宿主类型表示,反之亦然。
类型映射可以是非空的多对一、一对多或一一映射。
若类型映射是一一映射,其类型等价性同宿主语言的语义规则;否则,由类型的语义规则约定。
因需提供与作为宿主语言的 [ISO C++] 的互操作支持,所以明确约定实现中部分实体类型对应的 C++ 类型:
注释 string
是占位符,不要求是和 [ISO C++] 的 std::basic_string
相关的类型。但一般地,string
类型应具有和 std::string
相近的操作以便实现对象语言语义及支持互操作。
推论:字符串和词素可直接比较相等性或排序。
NPLA 数值在对象语言中具有数值类型,具体类型映射未指定,但在 NPLA 数学功能提供可选实现。派生实现可显式扩充或替换定义其它数值类型的类型映射。
其它宿主类型由实现定义。具体宿主类型参见以下各节和对象语言类型对应的描述。
宿主类型在对应的 C++ API 中可能以类型别名的形式引入。
原理
类型系统是开放的,可能提供不被对象语言支持的宿主语言类型和值。
但符合已指定的类型的实体需能被视为同种类型的实体使用,即子类型。
注释
非典型的宿主类型可以是特定的宿主类型的值的子集,即便这样的类型不被宿主语言的类型系统直接表示。
不被对象语言支持的值的一个例子是实现使用的中间值(thunked value) 。
关于中间值、string
类型的具体要求、NPLA 数学功能的规格说明和由实现定义的命名空间,参见 YSLib 项目文档 doc/NPL.txt
。
一般地,NPLA 规则不排除未定义行为。其中,宿主语言的未定义行为是非特定体系结构或其它 [ISO C++] 意义上不可预测或不可移植的行为。
除非派生实现另行指定,NPLA 约定仅有具有以下情形的程序引入未定义行为:
-
互操作中引起宿主语言的未定义行为或不满足约定的要求而可能引入派生实现定义的未定义行为。
- 注释 例如,没有被支持的并发访问引起宿主语言的未定义行为。
- 本机实现无法提供资源而引起宿主语言的未定义行为(如宿主语言的实现无法提供宿主语言函数调用的自动对象隐式使用的资源)。
- 违反资源所有权语义约束的操作,包括但不限于:
- 违反内存安全的操作。
- 注释 例如,违反按项对象和关联对象所有权的推论不能确保满足生存期要求的操作。
- 除非另行指定,构造任意的循环引用。
- 违反内存安全的操作。
- 使用特定的词法构造。
除非派生实现另行指定,NPLA 约定:
原理
满足错误条件的程序可能引起错误,也可引起未定义行为而不要求引起错误。这允许减少实现的复杂性。
对宿主语言的未定义行为的单独处理允许描述互操作。
程序的执行允许宿主语言的未定义行为,同时允许形式上不可靠,但仍可通过宿主的外部环境提供附加保证的实现,而保留可实现性:
- 典型地,宿主语言不保证调用的活动记录总是可用。
- 例如,[ISO C++] 指定程序在自动对象无法分配时具有未定义行为。
- 这种情形形式上无法排除,但不影响实用(否则,任意 [ISO C++] 程序都是不可移植的)。
- 实现仍应保守使用资源,以尽可能地避免引起宿主语言的未定义行为。
- 通过宿主的外部提供附加保证的实现类似保证为完整性的前提下通过加入附加的限制来使设计符合要求。
对管理规约的约定同时蕴含对 NPLA 实现的要求。这保证未定义行为不会被任意地在对象语言以外被引入。
注释
为简化互操作实现,部分 NPLA 未定义行为可能在实现中被检查以预防(尽可能避免)宿主语言的未定义行为,但这种检查不保证完全覆盖所有引起未定义行为的条件,不应预期其行为可移植。
关于构造循环引用可能引起的问题,另见内存泄漏。
一般地,本机实现要求资源分配失败时,引起(可能派生)std::bad_alloc
或另行指定的宿主异常而非宿主语言的未定义行为;但因为宿主语言缺乏保证,可能并非所有宿主语言实现都能保证实现这项特性。
实际的实现中非极端条件下(如宿主调用栈接近不可用)通常可支持实现这些行为。
宿主语言实现支持时,具有可预期的失败(而 NPLA 或宿主语言的非未定义行为)的 NPLA 实现的要求称为常规宿主资源分配要求。
宿主语言的 API 提供嵌套调用安全(nested call safety) ,当且仅当:
若调用没有宿主语言无法分配资源的未定义行为,则同时避免因宿主语言的嵌套调用深度过大时引起的这样的未定义行为。
嵌套调用安全应包括支持可能通过对象语言构造的输入使对应宿主语言的操作中的嵌套调用不保证的情形。
对象语言的实现可假定限制避免无限创建活动记录即满足嵌套调用安全的要求。
原理
嵌套调用安全允许不限制嵌套深度的可靠的调用,如递归调用。
宿主语言实现在宿主语言的尾上下文可能支持宿主 TCO 而使递归调用满足嵌套调用安全,但这并不是语言提供的保证,不应在可移植的实现中依赖。
[ISO C++] 并没有明确指定关于深度的限制,嵌套调用可能因资源耗尽而引起未定义行为。
严格来说,这种未指定深度是可移植性上的缺陷,因为任意小的深度的调用(甚至深度为 1 的非嵌套调用)都可引起未定义行为而不需要遵循任何 [ISO C++] 的要求,却仍然满足实现的符合性。
[ISO C] 也有相同的问题。
实际实现中,具体深度限制依赖实现。在宿主语言缺乏保证的状况下,添加附加假定对可实现性是必要的。
注释
对应宿主语言的操作中的嵌套调用不保证的情形的主要例子是保证宿主语言中立。
非嵌套调用安全的情形在过程嵌套调用深度过大时,可因为宿主语言的存储资源消耗导致的宿主语言实现的未定义行为,典型地包括实现中的栈溢出(stack overflow) 。
不限深度的重入不一定引起无限的活动记录的创建:尾调用应能保证嵌套调用安全。
当前所有 NPLA 实现中都没有显式的并发访问控制,但可通过互操作引入。
注释
一般地,为避免并发访问引起的宿主语言的未定义行为,需要通过本机实现在外部使用不同的资源实例或附加适当的同步。
另见并发访问安全。
除类型映射,NPLA 约定能作为一等对象的类型支持的抽象的类型,作为实现的最小要求的一部分。
以下章节扩充 NPLA 的其它类型,这些类型中的一部分可能作为一等对象。
基于开放类型系统,派生实现可定义其它类型,不论是否被互操作支持。
原理
这些类型在求值算法等规则的描述中适用。
两个不同对象可作为元素(element) 构成有序对(ordered pair, pair) 。
有序对的元素是子对象。
当且仅当若有序对的两个元素不同,交换元素得到的有序对和原有序对不同。
注释
一些编程语言中,构造有序对的操作称为 cons
,有序对又称为 cons 对。
空列表(empty list) 是不含有子对象的列表。其它列表是非空(nonempty) 列表。
每个非空列表是一个有序对对象,满足:
- 有序对对象的第一个元素是列表的元素。
- 若有序对对象的第二个元素是有序对,则这个有序对对象的第一个元素是列表的元素;否则,最后一个不是有序对对象的子对象是列表的元素。
注释 推论:同一个列表的元素不是另一个元素的子对象;不同元素之间不具有所有权,生存期不相交。
从非空列表对象中取得元素分解(decompose) 列表对象。若经有限次分解,不再可取得列表对象的元素,则列表对象被完全分解。
完全分解的列表的最后一个元素之外的其它元素是列表的前缀(prefix) 元素。
对象具有前缀元素,当且仅当对象是列表且具有前缀元素。
真列表(proper list) 是空列表,或能经完全分解得到最后元素是空列表的列表。其它列表是非真列表(improper list) 。
注释 推论:非真列表是非空列表。
广义列表(generalized list) 是真列表或非真列表。
广义列表的元素是一等对象。广义列表对元素具有所有权。
广义列表是完全分解的元素的序列(sequence) 。
作为广义列表的非真列表是无环的(acyclic) ,不包含环(cycle) 。
注释 同一般的 NPL 约定,NPLA 对象不支持自引用和循环数据结构。
除非另行指定,以下列表指真列表。
子有序对(subpair) 是一个有序对完全分解的序列中的元素的真子集构成的子对象。
子列表(sublist) 是一个列表中的元素的真子集构成的列表子对象。
注释
无环非真列表和真列表类似,可通过 cons
逐次构造。
非列表的有序对的元素可能具有自引用,而不是广义列表的元素,因此不是广义列表。NPLA 的一等对象不支持这种情形。
符号(symbol) 是未被求值的非字面量记号的类型。
符号值可构成名称表达式。
NPLA 使用统一的模型对存储和对象进行抽象,并提供关于存储、对象和作为对象的表示的项以及子项的若干保证。
对象语言的存储被视为资源进行管理,称为存储资源(memory resource) 。
原理
语言中默认不引入非一等对象。因此,存储和对象模型作用到所有实体,有助于保持简单性。
注释
一等对象的使用可能受到其它规则的限制,不总是能同时通过对象语言的构造创建和访问。
NPL 允许派生实现引入实体的规则不受限制。
因需提供宿主语言互操作支持,除不支持静态(static) 存储和没有提供支持的存储操作外,NPLA 的基础存储模型和对象模型和 [ISO C++11] 相同。
当前不支持的存储操作包括分配函数(allocation function) 取得的存储和线程局部(thread-local) 存储。
NPLA 还允许类似对象具有未指定的存储或不需要存储的实体,以使一等实体可涵盖宿主语言在功能上等价的非对象类型(如 C++ 的引用)。这些实体若被支持,其存储实现和互操作接口由派生实现定义。
保证存储性质的差异不被依赖时,不区分一等实体和一等对象的实现方式。
在此情况下对象都是固定(pinned) 的,即对象在存储期(storage duration) 内具有宿主语言意义上的确定不变的地址。派生实现可约定扩展作为例外。
推论:若一等实体不是一等对象,存储可能和一等对象的存储方式不同。派生实现可在必要时约定与其它一等实体存储的差异。
对象的生存期是存储期的子集。创建对象基于已确保可访问的存储;销毁对象结束后释放存储。
NPLA 支持特定的非一等对象作为引用值的被引用对象。
注释 和宿主语言类似。
作为一等对象相同方式传递的一等实体都视为一等对象。仅当不依赖一等对象的性质时,实现以非一等对象的方式实现一等实体的操作。
原理
实体的内部表示满足实现环境的要求决定和 NPLA 和宿主语言之间共享一些基本的假定。
特定的值是间接值(indirect value) 。
间接值可以关联(associated) 一个对象。通过间接值可以间接访问这个对象。
非一等对象的间接值由实现定义,参见 YSLib 项目文档 doc/NPL.txt
。
派生实现可以定义其它间接值,称为 NPLA 扩展间接值。
一个间接值有效(valid) ,当且仅当存在关联的对象且访问对象不引起未定义行为。
其它间接值是无效(invalid) 的。
除非另行指定,通过无效的间接值试图间接访问关联的对象不满足内存安全而引起未定义行为。
有效的引用值可能被无效化(invalidate) 而不再有效。
派生实现可指定能使间接值无效化的操作。
因关联的对象存储期结束而被无效化的间接值是悬空(dangling) 的。
原理
间接值可用于代替非间接值,避免求值时改变环境所有的非临时对象的所有权。
间接值可实现和 [ISO C++] 引用类型的表达式类似的行为。
间接访问默认没有对象的生存期检查,因此不是安全的。这可能被具体的间接值的规则改变。
限制具体的操作能避免或减少在可能访问间接值的操作随意引入具有潜在未定义行为风险。
注释
作为一等对象的间接值可能允许复制或转移关联的对象以恢复对应的非间接值作为一等对象直接访问。
在使用约定后,本节以下约定要求被 NPLA 实现支持作为一等对象的间接值。非一等对象的间接值由实现定义。派生实现可以定义其它的 NPLA 扩展间接值。
间接值生存期规则:被规约对象中间接值的生存期被引用的环境中的对象的生存期的子集。
不满足间接值生存期规则的情形,除非提供派生实现定义的其它保证,不保证内存安全。
以含间接值的项替代不含间接值的项,称为引入(introduce) 间接值。
包含间接值的项可被不含引用值的项替代,称为消除(eliminate) 间接值。
在特定的适当情形下实现应复制或转移间接值关联的对象以保证满足生存期要求,包括:
除非另行指定引起错误,若不能满足上述适当情形条件,则行为未定义。
派生实现可基于本节约定其它规则。
原理
为保证间接访问关联对象的内存安全,约定间接值生存期规则。
参见局部间接值安全保证和返回值转换。
注释
如需直接替换项表示的值,需消除间接值。否则,没有必要提前对项进行操作以提前移除间接值。
关于实现定义和派生实现定义的其它情形,参见 YSLib 项目文档 doc/NPL.txt
。
另见被求值的被规约项中的对象的所有权。
项引用(term reference) 作为间接值引用一个项,访问这个以这个项作为表示的被引用对象作为关联对象。
项引用具有标签。
被求值的表达式的内部表示中的对象具有 NPLA 对象的所有权。
这些内部表示包括环境对象或被求值的表达式中的项的情形。
对象是表示它的被规约项的项对象(term object) 。
NPLA 临时对象的存储未指定,但部分临时对象被项所有。
求值结束而不被使用的项的资源在求值终止时被释放,包括被项独占所有权的这些临时对象。
求值终止包括可被实现确定的异常退出。
对名义上被项所有的临时对象,必要时实现可分配内部存储转移项(包括在环境中分配),以满足附加要求(如生存期附加约定)。
对象的所有权随可随对象被转移,参见对象的复制和转移。
求值结果可以是:
注释 按求值规约,其它的求值结果的存在未指定,若存在则可能需要其它处理,可能依赖和处理一等对象的值不同的语义规则。
函数调用时以活动记录保持被引用对象的所有权。活动记录及其帧的具体结构、维护方式和生存期由派生实现定义。
除非另行指定,NPLA 只有一种作用域,这种作用域中的名称由环境提供。
除非另行指定,NPLA 的活动记录不需要和宿主语言的结构保证直接对应关系。
原理
因为宿主语言函数调用实现(典型地,调用栈(call stack) 及其中的栈帧)不提供可移植的互操作,不要求实现提供活动记录之间的映射关系。
注释
临时对象的存储未指定、异常退出和所有权转移类似宿主语言。
结果对象和 [ISO C++17](由提案 [WG21 P0135R1] 引入)中的概念对应。
仅在泛左值中允许引入可能访问关联对象的间接值。
推论:泛左值的项对象和它作为间接值可关联的对象(若存在)不是临时对象,被环境所有。
通常纯右值作为其它项的子项而被独占所有权,求值时可能通过临时对象实质化转换标识创建的临时对象。
表示临时对象的项被纯右值所有,也间接被其它项所有。
特定的纯右值可能被环境所有,但应只通过复制等方式访问其值而不依赖所有权关系。
关于实现中项的宿主类型和构成以及纯右值被环境所有的例子,参见 YSLib 项目文档 doc/NPL.txt
。
原理
基于间接值的性质,为保证内存安全,避免非预期地超出存储期的间接值访问,限制引入间接值的表达式的值类别。
因临时对象可能具有和一等对象不同的表示,在此特设规则约定。
蕴含按抽象机语义不等价副作用的并发的访问是冲突的(conflict) 。
不共享相同的控制状态的无序的规约事件是潜在并发的(potentially concurrent) 。
若程序包含蕴含冲突的作用的潜在并发的求值,且这些求值之间没有附加的数据竞争避免(data race avoidence) 保证,程序的执行包含数据竞争(data race) ,不满足并发访问的内存安全。其中,以下机制数据竞争避免保证:
- 所有潜在并发的求值都是宿主实现提供的原子操作(atomic operation) 时,避免数据竞争。
- 派生实现另行指定的数据竞争避免机制。
并发访问相关的概念和 [ISO C++11] 相容。
(非并发)内存安全(memory safety) 是存储资源避免特定类型不可预测错误使用的性质。
基本的内存安全保证蕴含非并发访问时不引入未定义行为。这至少满足:
注释 实现仍可能因其它规则引起未定义行为;特别地,这包括本机实现无法提供资源的未定义行为。
派生实现可能扩展内存安全,提供语言规则避免非预期的内存访问错误,提供更一般的高级安全(security) 保证。
注释 例如,保密性(secrecy) 和完整性(integrity) 。
除非另行指定,派生实现不提供扩展的内存安全保证。
不满足并发访问安全的访问是非内存安全的。
原理
关于内存安全含义的讨论,另见这里。
注释
用户代码应注意避免违反内存安全的访问,包括非并发的,以及并发访问的内存冲突。
非内存安全操作是不保证内存安全的操作,在对象语言中即可能引起违反内存安全。
这些操作违反内存安全时,引起 NPLA 未定义行为,且可能未被实现检查而同时引起宿主语言的未定义行为。
对象语言中的非内存安全特性可能直接调用这些操作。NPLA 外依赖此类操作的其它操作也具有类似的性质。
注释
派生实现或用户程序可能使用补充检查等方式避免未定义行为。
NPLA 中,确定地引入具有非内存安全操作的对象的操作应仅只包括引入特定的间接值或其它派生实现指定类型的值的操作:
排除非内存安全操作以及非内存安全的本机实现,NPLA 实现的对象语言提供基本内存安全保证。
满足 NPLA 对象语言内存安全保证同时排除引起宿主语言未定义行为的非内存安全的操作,NPLA 实现提供基本内存安全保证。
注释 宿主语言未定义行为的非内存安全的操作如超出生存期的访问。
除非通过接口约束另行指定,使用 NPLA 实现的派生实现应提供相同的保证。
运行时检查可能帮助排查内存安全的实现行为。这包括蕴含运行时检查的接口约束(失败时抛出异常或断言)。
此外,实现可能提供可选的运行时检查。这些可选的检查帮助排查未定义行为,而不应被程序实现依赖。
访问间接值涉及维护内存安全保证时,可能需要提升项消除间接值,以移除允许非内存安全访问的间接值。
原理
使用删除策略实现过程调用时,其中分配的局部(local) 资源随包含资源引用的引用返回可能逃逸。一般的间接值也有类似的逃逸问题。
若其关联的对象(如项引用关联的被引用对象)在调用后不再存在,则间接值不再有效,构成悬空间接值。若这些间接值被调用者获取(如被作为返回值传递),继续访问这个间接值关联的对象非内存安全。
为维护内存安全保证,这些情形应被避免,如通过:
- 通过分析调用处的代码证明确保不存在这样的内存不安全访问。
- 通过间接值的消除移除这些间接值使这种悬空间接值在调用者中自始不存在。
替代消除间接值的方式包括通过逃逸分析(escape analysis) 替换间接值,这也能减少间接值的访问而提供更优化的实现。例如,通过对环境中被绑定对象的使用进行逃逸分析提供优化实现。
但是,这不在 NPLA 中被要求,因为:
- 逃逸分析需要完整的所有权信息,这需要附加的开销,否则不总是可行(例如涉及跨多个过程的调用)。
- 对删除策略,逃逸分析也没有提供不可替代的优化。
资源泄漏(resource leak) 是不能预期地(决定性地)访问之前被分配的资源的情形。
内存泄漏(memory leak) 是存储资源的泄漏。
强内存泄漏状态是指存在存储无法通过任何途径访问的状态。若存在存储不被任意对象或其它另行指定的代替对象的实体(如宿主环境)所有权的传递闭包包含,即所有权依赖不可达(unreachable) ,则存在强内存泄漏。
弱内存泄漏是除了强内存泄漏以外的内存泄漏,和具体预期相关。
原理
一般意义下,[Cl98] 中定义的任一空间复杂度类都可以作为形式的预期。因为内存作为存储资源被空间复杂度类度量,满足某个空间复杂度类的无空间泄漏(space leak) 蕴含对应的无内存泄漏。
弱内存泄漏的预期的可实现性和实现细节相关,因此 NPLA 不指定具体预期。
单一作用域内的资源回收有删除(deletion) 和保留(retention) 的策略。
NPLA 不限定具体使用的回收策略,但要求应支持:
- 释放一等对象时允许具有副作用。
- 确保副作用作用时机的确定性,即除用户程序指定外,不在抽象机语义中延迟副作用的起始。
为简化语义规则同时避免限制特定的可用资源(如系统中剩余的内存)的变化被派生实现抽象为副作用,除非派生实现指定,不对内存使用保留策略,不使内存超出对象生存期。
NPLA 要求实现完全避免除用户程序显式管理资源的资源泄漏以外的强内存泄漏。
除非另行指定,NPLA 释放资源的作用顺序未指定。NPLA 不依赖释放的作用的顺序。
派生实现可以要求使用不同的规则:
- 指定释放资源的顺序。
- 可选地支持非确定的释放资源的副作用。
NPLA释放可能具有的副作用顺序的存储资源和其它资源共享更普遍的所有权抽象资源的所有权语义上的操作:
使用删除策略时,活动的过程调用对其中分配的资源具有所有权。
注意多个对象构成的系统中,仅存在平等的所有权时的循环引用造成强内存泄漏:除非即从循环引用的对象中区分出具有不同类所有权的对象子集实现所有权正规化,总是存在无法被释放资源的对象。
NPLA 不要求实现 GC 等机制避免这类循环引用。
关于循环引用避免,另见 YSLib 项目文档 doc/NPL.txt
。
原理
NPLA 不要求实现 GC。
未指定的资源释放的作用顺序使其中可能具有的副作用影响的可观察行为成为未指定行为。
除非派生实现要求使用不同的规则支持非确定的资源的副作用,NPLA 的实现不依赖不保证确定性释放资源的副作用顺序的追踪(tracing) GC 。这使追踪 GC 可能被可选地添加(opted-in) 到实现支持特性中。这允许自动资源管理机制中一定程度的变化的自由。[ISO C++11] 起直至 [ISO C++20] ,C++ 语言规则支持类似的策略。
资源释放副作用的确定性要求和作用顺序未指定的规则不影响实现使用基于引用计数的 GC 策略。这允许实现以简单的方式以用户程序不直接可见的方式引入共享资源,在避免资源泄漏的意义上兼顾正确性和简单性。但为避免单一所有者,此时在对象语言应提供特性使用户程序可以创建隔离共享者的资源实体。
基于非预期的循环引用不可避免地造成实现开销而违反避免不必要付出的代价(即使这种开销可能并不总是可观察)NPLA 不要求实现 GC 和对一般对象区分强弱引用等机制避免循环引用。此时,程序应自行避免所有权意义上的循环引用以避免资源泄漏。
由于 GC 通常基于具有特定操作的单一资源所有权的所有者的对象池的这一实现特例,不依赖共享所有者的 GC 的设计一般也更容易满足统一性、最小接口原则和关注点分离原则。
以上规则允许程序中:
关于不同的资源回收策略(其中一部分可能引起存储空间资源泄漏)的讨论,详见 [Cl98] 。
使用所有权抽象活动记录的资源能更好地满足资源管理机制和具体操作的可复用性和作用使用原则的要求。
派生实现可补充定义规则在资源回收的作用上提供更强的安全保证。
原理
内存泄漏是和内存安全不同的另一类非预期的问题,表明语言设计、实现或程序存在缺陷。
即便不违反内存安全保证,涉及弱化空间复杂度类预期的内存泄漏仍可损害程序的可用性而引起安全(security) 问题。
内存泄漏和违反内存安全同属违反特定的存储访问不变量的错误条件(error condition) ,但因为不论在语言还是程序的设计和实现中,避免的机制相当不同,在此被区分对待。
即便不扩展规则提供更强的内存安全保证,仅在资源回收的作用上避免错误条件也是有意义的。
存在其它语言使用类似的区分内存泄漏和非内存安全的设计,如 [Rust](详见相关文档)。
对象的子实体是对象时,子实体是对象的子对象(subobject) 。
除非另行指定,子对象及其性质同宿主语言的约定:在宿主语言的表示中表现为子对象的对象语言中的对象,也是对象语言的子对象。
对象语言的其它具有子对象的情形由派生实现定义。
对象对它的子对象具有平凡的所有权。
对象的子对象的生存期不先序对象的生存期起始,对象的子对象的生存期结束不后序对象的生存期结束。
对象的子对象的生存期起始后序对象的生存期起始,对象的子对象的生存期结束先序对象的生存期结束。
除非另行指定,同一个的对象不同子对象的存储期起始、存储期结束、生存期起始、生存期结束之间分别无序。
对象对其存储期和生存期的其它约束和宿主语言相同。
对象可通过子对象引用关联和与其生存期相关或无关的其它对象。
通过子对象访问的被引用对象上的副作用是否蕴含对象上的副作用未指定。
关于内部对象,参见 YSLib 项目文档 doc/NPL.txt
。
原理
子对象不一定支持可修改的一等状态。修改子对象可能导致或不导致对象或先前通过相同方式取得的子对象的改变。
[ISO C++] 通过类型定义具有的隐含的对象布局共享同类对象的内部表示。与之不同,为简化非一等对象表示的项上的操作,子对象之间不一定共享表示。
特别地,通过子对象引用项访问的对象的子对象之间不一定具有同一性。
关于具体表示,参见 YSLib 项目文档 doc/NPL.txt
。
注释
作为支持子对象作为内部对象的逻辑前提,NPLA 不支持循环引用。
[ISO C++] 允许 const
成员提供不支持修改的状态。NPLA 不要求类似的类型系统支持,没有类似的设计。
作为对象的子项是项对象的子对象。
因为子项可以递归地具有子项,项对象作为数据结构构成树(tree) 。项对象是树的节点,即项节点(term node) 。
项节点具有如下互斥的基本分类:
- 枝节点(branch node) 或非叶节点(non-leaf node) :具有子节点的节点。
- 叶节点(leaf node) :不具有子节点的节点。
除子项外,项具有值数据成员(value data member) 作为其子对象。
表示项对象的被规约项的值数据成员提供时间复杂度为 O(1) 的操作判断:
值数据成员可能具有空值。
值数据成员和子项可构成对象的内部表示:
- 列表节点(list node) 是值数据成员为空的节点,表示真列表。
- 空节点(empty node) 同时是叶节点和列表节点,表示空列表。
- 实现可定义其它的节点作为其它的内部表示。
若项存在其它子对象,作为对象内部表示的具体规则由实现定义。
满足以下条件的替换变换替代项或其子对象,称为项的提升(lifting) :被提升的项(源)是提升后得到的项(目标)的一个直接、间接子项或项的子对象变换得到的项。
提升可能包含附加检查,检查失败时可能引起错误而不实际进行提升。
除非另行指定,提升项修改被替换的对象。
原理
项的子对象确定的表示可能被具体的互操作依赖。
项的提升可以视为作为语法变换的消去 λ 抽象的 lambda 提升 (en-US) 的一般化,但此处和 λ 抽象没有直接关联。
项的提升的变换可以是恒等变换,即直接以子对象作为替换的来源。其它变换如创建间接值和取间接值关联的对象,对应的提升引入和消除间接值。
项的提升的检查可包括为满足接口行为的语义检查和实现为预防宿主语言的未定义行为的附加检查。
被提升的项往往被转移,因此一般地,需要在宿主语言中可修改。若被提升的项表示对象语言的值,一般也需要在对象语言中可修改。
除以上性质外,对象可关联其它元数据以指定对象的属性。
和属性对应的可组成对象的表示的非一等实体统称为标签(tag) 。
对象具有的标签决定以下正交的性质:
- 唯一(unique) 引用:指定对象的值关联到自身以外的不被其它对象别名的对象。
- 以唯一引用关联的对象进行复制初始化时,不需要保留关联的对象的值。
- 不可修改(nonmodifying) :指定对象的值保持不变。
- 若操作需要修改此对象,则引起错误。
- 临时(temporary) 对象:指定对象的值被临时使用。
唯一引用和不可修改是引用值的属性。对象语言中,引用值以外的对象是否具有这些属性未指定。为互操作目的可能具有实现定义的更强的假设。派生实现也可定义更强的假设。
临时对象属性类似唯一引用,但限定的可以是对象自身而非关联的其它对象,即引用值自身和被引用对象可以分别具有临时对象属性。但除了引用值属性外,临时对象属性仅限在临时对象上出现。
注释
不可修改的对象类似 [ISO C++] 的 const
类型的对象。[ISO C++] 的非类和非数组类型的对象不具有 const
修饰。
对象的标签不在大多数对象中可见。另见引用值的属性。
除非另行指定,环境维护的名称都是变量名。
NPLA 的求值环境可以是:
环境可引用若干个关联的其它环境为父环境(parent environment) ,用于重定向。
除非派生实现另行指定:
原理
如需求中指出的,本设计避免命名空间隔离,因此只有一种被环境支持且被求值算法统一处理的名称。
若派生实现需要,可修改环境的内部表示和求值算法的名称解析步骤以对不同的名称添加支持。相反,在已有多种名称的设计中还原为一种设计是更复杂和不可行的。因此,在本设计中不预设多种名称。
环境作为可保持可变状态的对象,是环境对象(environment object) 。
环境对象包含变量名到表示被绑定实体的映射,称为名称绑定映射(name binding map) ,实现变量绑定。
被绑定实体是对象时,称为被绑定对象(bound object) 。NPLA 环境对象中的被绑定实体包含一等对象,因此被绑定实体总是被绑定对象。
环境对象对其中的名称绑定映射具有独占的所有权。名称绑定映射对其中的对象可具有独占或共享的所有权。因此,环境对象可对包括被绑定实体的名称绑定映射中的对象具有独占或共享的所有权。
环境记录之间共享所有权,以环境引用访问。
父环境可共享环境记录。通过共享环境记录实现重定向的环境表示是链接的(linked) 而非平坦的(flat) 。
原理
仅在可证明符合语义要求等价时,使用平坦的环境表示。
对支持一等对象语义的设计,因为明确要求区分同一性,对象的存储不能被任意地复制。
一般地,仅在可证明父环境对应的环境记录在对象语言和实现内部都不被共享访问(不具有共享引用且不被别名),且不存在任意派生实现定义的对释放顺序引起的可观察行为差异时,才能唯一具有这个父环境的环境为平坦的表示而保持语义不变。
注释
变量名通过以和字符串一一对应的值表示,没有直接的值的限制,可能为空串。
若环境记录直接持有被引用对象,则这些对象是环境记录的子对象。
环境引用是对象语言中访问环境记录的一等对象。
注释 环境引用不是引用值。后者关联的被引用对象是一等对象。
环境引用共享环境对象的所有权。
根据所有权管理机制的不同,环境引用包括环境强引用和环境弱引用。
环境强引用可能共享环境对象的所有权,对环境对象的名称绑定映射持有的项具有间接的所有权。
作为间接值,环境引用可被复制或转移。
复制或转移环境引用不引起被引用的环境对象被复制。因此,按值传递环境引用不引起其中所有的对象被复制。另见引用。
原理
区分环境对象和环境引用在纯函数式语言不是必要的,因为不需要关心环境中的子对象的复制影响可观察行为。
否则,为支持影响可观察行为的环境的修改,非环境记录的环境引用是必要的。
环境引用也是一种较简单且一般普遍高效的父环境的实现表示,可直接实现链接的环境而不需要证明和实现特设的其它内部表示能和抽象机意义上链接的环境保持语义等价。
续延捕获若复制续延,可能引起关联的环境的复制,影响可观察行为并引起不必要的实现开销。为此,区分环境引用是必要的。
以环境引用作为一等对象使访问被引用对象等环境记录的子对象时需要间接访问,在环境实际不需要被复制的大部分其它场景引起开销。这种开销是可接受的,因为:
- 考虑到一等环境的普遍性,有必要有效支持对象语言中创建环境临时对象(而不仅仅是环境对象的引用值)的使用使之避免复制。
- 实现可能提供附加的证明以在优化的翻译过程中替换环境引用为环境记录或其它不需要间接访问的中间表示,以消除这些开销。
不论这样的证明是否存在,环境强引用和弱引用仍在对象语言中区分,以明确接口上的所有权语义。
引入环境弱引用作为一般的引用机制,且仅在必要时使用环境强引用,以避免过于容易引入循环引用引起强内存泄漏,符合适用性。
NPLA 对象语言中,表达式的求值隐含对应一个环境对象作为求值算法需要的上下文输入,称为当前环境(current environment) 。
本节约定对象语言中的表达式相关的语义规则,特别是求值规则。
表达式归类为具有以下基本的值类别(value category) 之一:
一个泛左值可能被标记为消亡值(xvalue) ,以提供基于不同的所有权的行为。
纯右值蕴含对象在可观察行为的意义上不被共享,类似不被别名的引用的被引用对象不被共享。
左值(lvalue) 是除了消亡值外的泛左值。
右值(rvalue) 是消亡值或纯右值。
基本的值类别、消亡值、左值和右值都是值类别。
求值涉及表达式的值类别仅在必要时约定。
表达式的值类别是上下文相关的,相同表达式构造在不同的上下文可能具有不同的值类别。
NPLA 表达式允许在源语言语法之外的形式被间接构造,这些表达式同样具有值类别。
求值规约可能重写一个表达式为具有不同值类别的为被规约项。即便不能被对象语言表达,只要不和其它语义规则冲突,它们在此被视为其它形式的表达式的表示,即项对象也对应地具有值类别。
一般地,NPLA 的表达式不限定从源代码的翻译确定,且一个表达式的求值结果不排除继续构成表达式而被求值,因此表达式的值也普遍具有值类别。
除非另行指定,若一个 NPLA 表达式没有指定未被求值,则其值类别是其求值结果的值类别。
原理
值类别根据是否只关心表达式关联的(对象的或非对象的)值,在需要对象时提供区分两类一等实体的机制,同时避免在仅需要表达式关联的值时引入不必要的其它对象。
注释
对象语言表达式的值类别和 [ISO C++17](由提案 [WG21 P0135R1] 引入的特性)类似。
值类别在 [ISO C++] 中实质上是一种静态类型系统。在 NPLA 中以更灵活的可在运行时访问的元数据代替,仍能体现类似的上下文相关性。
除了标记消亡值,附加其它元数据也允许区分不同的所有权行为。
NPLA 值类别和 [ISO C++] 也有显著的不同,体现在如下扩展:源语言语法外的被规约项的项对象视为 NPLA 表达式,也具有值类别。
因此,作为求值结果的表达式的值也普遍具有值类别。若存在结果对象,可直接通过其类型确定。
作为静态语言,[ISO C++] 缺乏允许在运行时确定的求值特性,这些不同不在 [ISO C++] 中可用,可以被视为保守的扩展。
NPLA 中,值类别作为实体类型,被作为一种内建的类型系统。
注释
这和 [ISO C++] 不同。[ISO C++] 的“类型”的定义排除值类别,尽管值类别具有类型论意义上所有可作为类型讨论的对象的性质。
另见引用类型。
对象被创建后可通过初始化(initialization) 决定其值,并可能存在其它作用。被决定的值是初始值(initial value) 。
决定初始化这些作用的表达式是初始化的初值符(initializer) 。
初值符的求值可能有副作用,其求值结果指定特定被初始化的对象的初始值。
初始化包括被绑定对象的初始化和作为函数值的返回值对象的初始化。
初始化被绑定对象可能以修改操作的形式体现,此时修改绑定具有副作用。若这样的副作用存在,每个被初始化的值后序于对应初始的计算。
注释
初值符的求值的副作用不属于初始化,其求值结果和对象的初始值不一定相同。
和宿主语言不同,初始化不是独立的依赖特定语法上下文的概念,但此处语义上的作用类似。
对象的初始化一般可蕴含子对象的初始化。
初始化包括直接初始化(direct initialization) 和复制初始化(copy initialization) 。
函数可能接受引用值参数和返回值,是对函数的形式参数或函数值的复制初始化;其它初始化是直接初始化。
复制初始化形式参数和函数值时,函数参数或返回值作为初值符。
注释
区分两者和宿主语言类似。
部分函数可保证被初始化的对象副本中的值和初值符的值及元数据一致。
这样的参数或返回值的初始化的求值称为转发(forwarding) 。
转发也包括只部分保留上述部分元数据的情形。
在允许保留元数据不变的上下文,转发在本机实现中可直接通过转移项实现。
转发保持引入这些初始化的表达式(通常是被求值取得函数值的函数表达式)时,其求值结果(函数值)的值类别和初值符保持一致。
注释
这里的元数据的一个例子是引用值的属性。
转发类似宿主语言的完美转发(perfect forwarding) 。
另见函数值传递。
可使用初值符为参数进行复制或转移操作以复制初始化对象,创建对象的副本。
注释 这类似宿主语言中的类类型的值。其它情形另见复制消除。
对象的复制和转移不改变被转移后的类型。
对象的复制和转移对应蕴含其子对象被复制和转移。在互操作的意义上,若项具有子对象的独占所有权,这些子对象的复制构造函数和转移构造函数被对应调用。特别地,这里的子对象包括宿主值。
可使用转移操作时,不对作为对象的表示的项进行复制,因此不要求其中的子对象可复制,而避免引起错误。
注释 这类似 [ISO C++11] 起选择类的转移构造函数代替复制构造函数。
和 [ISO C++11] 起不同,上述可使用转移操作的条件和语法上下文无关:引起选择转移操作的条件由对初值符的谓词而非类似宿主语言的构造函数判断(详见默认值类别转换约定)。
注释 同宿主语言。
除非另行指定,需要创建实体的副本时:
- 若对象满足可转移条件,则转移而不是复制。
- 其它情形实体被复制。
注释 一个主要的实例是按值的副本传递。
一定条件下,作为对象的表示的项可被整体转移,而避免其中包含的对象的初始化在对象语言中具有可见的作用。
在互操作的意义上,因作为对象的表示的项的转移,项及其子对象的转移构造函数会被调用,但项的值数据成员中的宿主类型的转移构造函数不会被调用。
注释 这一般要求实现使用某种类型擦除使子对象类型的转移构造函数的调用不蕴含宿主类型的转移构造函数的调用。
项的转移是析构性转移。
一般地,当对象需要被转移且没有约定转移后要求类型不变时,项的整体转移可代替对象的转移,避免初始化新的宿主对象,称为宿主对象转移消除。
注释 若需调用宿主类型的转移构造函数,需明确避免在代替对象的转移的上下文中进行操作。派生实现可提供这些操作。
返回值转换上下文的转移蕴含宿主对象转移消除。
若被复制消除的对象来自不同的项,则复制消除蕴含宿主对象转移消除。这包括所有对象转移的返回值转换上下文的情形。
在对象语言中,引用值(reference value) 是作为引用的值,可保存在一等对象中。这样的一等对象是引用对象(reference object) 。
引用值和引用对象的值具有引用类型(reference type) 。
在特定上下文中,引用和其它一等对象的值的相同具有不同的语义,主要体现在引用值被按值直接初始化传递和按引用传递时。
注释 差异和 [ISO C++] 中使用对象类型和引用类型作为参数类似。
NPLA 引用值总是假定和被引用对象关联。
注释 和宿主类型类似,引用类型没有空值。
仅当以下情形中,NPLA 引用值的被引用对象是非一等对象:
原理
由于左值的项对象被环境所有,为允许规约求值其中的被绑定对象,需要不被环境所有的(其它不同的)被规约项作为表示的项对象作为中间值。
这种中间值通过间接引用作为一等对象使用,也是一种间接值,即引用值。
特定的引用值是子对象引用(subobject reference) ,其被引用对象是被另一个对象所有的、作为这个对象的子对象的一等对象。
子对象引用对特定操作可表现和其它一等对象不同的行为。
以下引用是子对象引用:
语言可能引入其它的子对象引用。
作为一种间接值,引用值有效当且仅当访问被引用对象不引起未定义行为。
以下约定要求被 NPLA 实现支持的有效的引用值总是无条件地允许访问对象。
有效的引用值应通过特定的构造方式引入,包括:
- 在对象语言通过被引用对象初始化引用值。
- 互操作引入的保证不引起未定义行为的引用值。
注释
一些对象语言的操作可能引起引用值无效。例如,改变被引用对象可以使已被初始化的有效的引用值成为悬空引用(dangling reference) 。
被引用对象也可以是引用值。
被引用对象不是引用值的引用值是完全折叠(fully collapsed) 的。
除非另行指定,未折叠的(uncollapsed) 引用值指未完全折叠的引用值。
注释
这和宿主语言不同。
引用值可以具有和作为引用值表示的项保存的属性相互独立的属性,保存其作为一等对象的状态。
属性不可分割:一个引用值明确具有或者不具有一种属性。
和对象属性对应,NPLA 指定的引用属性可以是:
- 唯一引用。
- 不可修改引用。
- 临时对象引用。
引用值属性指定通过引用对被引用对象的访问假定允许具有的性质,即便被引用对象自身没有具有这些属性。
特定的操作使用引用值作为操作数,根据不同的属性决定行为,包括在违反属性引入的假定时引起错误。
在本节要求以外,除非派生实现另行指定,违反这些假定不引起 NPLA 未定义行为。
具体的引用属性满足以下语义规则:
- 唯一引用允许通过引用值访问被引用对象时,对象可被假定不被其它引用而仅通过这个途径访问,即便实际存在其它途径的引用时可能引起不同的行为;在假定的基础上程序具有何种可能的行为是未指定的。
- 唯一引用可被假定不被共享,被引用对象不被别名。
- 通过不可修改引用的左值的对象访问不包含修改。否则,若没有引起错误,程序行为未定义;但除非另行指定,不引起宿主语言的未定义行为。
- 具有临时对象引用属性的引用值是临时对象的引用值,其被引用对象是临时对象。
原理 宿主语言的互操作不被总是要求保证对象语言程序的可移植性,但不应引起实现自身的行为无法预测。
对引用值的操作传播(propagate) 特定的引用属性,当且仅当:
若操作数是具有特定引用属性的引用值,且结果是引用值时,结果具有和操作数相同的特定属性。
注释
引用值属性和对象属性相互独立,类似 [ISO C] 和 [ISO C++] 在指针和引用等复合类型的 const
等限定独立于指向的对象或被引用对象上的类型不同。通过 const
等属性可以在指针或引用类型上单独限制类型,而不影响对应的被间接访问的对象。
唯一引用蕴含的假定类似 [ISO C] 约定的 restrict
关键字,但程序违反假定的约束时不引起未定义行为。
和 [ISO C++] 核心语言(但不是 [res.on.arguments] 中的标准库绑定到右值引用实际参数的约定)的右值引用类似,唯一引用不总是表示被引用对象不被共享。
接受唯一引用的操作可能只假定被引用对象的子对象不被共享,也可能完全不进行假定,这依赖具体操作的语义。若需要和具体操作无关的无条件非共享假定,使用纯右值而非作为左值的唯一引用。
和宿主语言的 const
限定类型类似,不可修改引用仅针对特定左值的访问;通过共享的其它未被限定的引用仍可修改对象。
违反不可修改引用引入的假定的错误可能通过类型检查或其它方式引起。
临时对象引用类似 [ISO C++] 的转发引用(forwarding reference) 中保留在表达式声明中的类型信息。
因为 NPLA 不支持声明元数据,这些信息保存在对象的表示中,且在初始化时被引用值保存;也因此这些元数据可跟随一等对象传递。对临时对象,绑定操作可确保元数据被添加。
这也和宿主语言不同。在宿主语言中:
- 无论是标记消亡值的右值引用类型还是标记是否可转发的引用的转发引用推断的类型信息(左值引用或右值引用)都是静态的。
- 并且,转发的类型信息只在函数模板的局部有效,而不存在对应的跨过程传递机制。
作为间接值,引用值可被消除,即被其(可能多重)引用关联的被引用对象替代。
未折叠的引用值消除一次引用值,结果仍是引用值。
推论:因为引用值不循环引用自身,除非引用值已完全折叠,继续消除引用值得到的值和引用值是不同的值。
原理
特定的引用值消除可蕴含对不可修改的传播的要求。这和 [ISO C++] 初始化引用时遵循的 const
安全性,属于类型安全性的一种。
但是,消除引用不一定总是预期这种性质,特别当折叠不被预期时。
例如,[ISO C++] 内建指针的不同级 const
不会被隐式转换直接折叠合并。消除间接的指针值不是隐式的(而依赖内建一元 *
操作符),这是因为指针作为类型构造器自身的类型安全需要;是否消除 const
限定符仍然需要基于其它理由考虑。
而当被引用对象实现子对象时,修饰被指向的类型的 const
不会自动传播到子对象的类型中,此时可有 std::experimental::propagate_const
可选引入这种性质。
对具有非间接访问的子对象的类型,这相当于 [ISO C++] 的 mutable
修饰符,可实现内部可变性。而允许子对象以外直接不传播不可变性,是一种结构性的平凡的扩展:这允许把被引用对象直接视为一种子对象的实现,而非要求引入新的名义类型。
在 NPLA 这样没有要求显式类型编码是否可变的语言中,首先要求总是具有不可修改的传播性质会显著增加规则形式上的复杂性。若具体操作需要传播不可修改性,仍可进一步约定。
注释
典型地,消除引用值包括:
和引用折叠不同,引用值提升转换不满足对不可修改引用属性的传播性质。
和 [ISO C++] 类似,引用值在 NPLA 中默认不被继续引用,使用引用初始化引用会引用到被引用对象上,即引用折叠(reference collapse) 。
引用值被折叠后结果和原引用值不同,当且仅当原引用值是未折叠的引用值。
和 [ISO C++] 不同,NPLA 不限制派生实现利用未折叠的引用值。
注释 特定的操作可能区分未折叠的引用值。
引用折叠的结果是不可修改引用,若引用值和作为引用值的被引用对象之一是不可修改引用。
引用折叠的结果满足不可修改引用属性的传播性质。推论:
原理
内部表示可支持间接的引用,以允许在对象语言中实现一等引用。
引用折叠对不可修改的传播性质的要求和 [ISO C++] 的引用折叠对 const
限定符的处理类似。
引用折叠对唯一引用的要求和 [ISO C++] 的右值引用仅通过被折叠的引用都是右值引用类型折叠类似。注意 [ISO C++] 右值引用推断仅用于推断转发引用(forwarding reference) 参数,而非直接声明特定的右值引用类型。
和唯一引用不同,临时对象相对唯一引用更接近 [ISO C++] 的声明的右值引用类型信息(而非推断值类别时使用的消亡值表达式的右值引用类型),一般不预期被折叠。
注释
未折叠的引用值被折叠时,用于初始化的被引用对象可能仍然是未折叠的引用值。
根据项是否具有特定元数据的引用值可判断使用复制代替对象转移的条件。
对象的可转移(movable) 条件的判断基于首先基于值的类型:
- 非引用值(纯右值)总是可转移的。
- 否则,对象是引用值。可转移由引用值的属性决定:当引用值是唯一引用且非不可修改,引用值是可转移引用,对应的被引用对象是可转移的。
作为引用值的表示,引用项(reference term) 是包含项引用的项。
引用项中的项引用对象引用一个(其它的)项,即被引用项(referenced term) ,用于在必要时引入可被引用的一个项而不在 TermNode 中直接储存这个项的值。
被引用项表示引用项作为引用值对应的被引用对象。
临时对象可作为引用值的被引用对象。
与此不同,非临时对象的引用值可作为一等对象而总是需要区分作为不同对象的同一性。
带有临时对象属性的引用值可在特定的操作中被视为和临时对象引用近似的引用值。
子对象引用的表示是子对象引用项(subojbect reference term) ,和本节中的其它引用类型的表示兼容,但不完全相同。
关于引用项的构成,另见 YSLib 项目文档 doc/NPL.txt
。
原理
因为临时对象不是一等对象,临时对象的引用值可代替关联的被引用对象使之作为一等对象被访问。
为在对象语言中区分引用值和非引用值的一等对象是必要的,引用项这样的特设表示是必要的。
非引用项的表示则是针对临时对象的一种优化,因为使被引用对象总是在作为引用值的表示而:
带有临时对象属性的引用值和临时对象的引用值不同,参见绑定临时对象属性。
根据表示和属性,引用类型具有如下子类型:
- 左值引用(lvalue reference) :以引用项表示的非唯一引用。
- 右值引用(rvalue reference) :以引用项表示的唯一引用。
引用值是否作为左值使用取决于上下文。除非另行指定,引用值都是左值。
注释 在要求右值的上下文发生左值到右值转换。
引入不同的引用子类型后,NPLA 一等对象的值的类型和值类别存在以下一一对应关系:
- 若类型是左值引用,则对应的值类别是左值。
- 若类型是右值引用,则对应的值类别是消亡值。
- 否则,对应的值类别是右值。
原理
左值引用和左值引用与宿主语言中的对象类型的左值引用与右值引用分别类似。
注释
在要求右值的上下文,作为左值的引用值发生左值到右值转换。
特定的引用值是不安全引用值(unsafe reference value) ,可能和常规的其它引用值具有不同的内部表示。
若实现支持不安全引用值,和其它引用值的行为不同由实现定义。
派生实现可能添加更多对不安全引用值的假设。
原理
不安全引用值可能放弃常规的引用具有元数据而能被更高效地访问。
具有特定值类别的表达式可转换为不同值类别的表达式:
- 除非另行指定,泛左值总是允许作为纯右值使用。从泛左值取对应右值的操作称为左值到右值转换(lvalue-to-rvalue conversion) 。
- 从纯右值初始化可被对象语言作为一等对象使用的临时对象的引用值作为消亡值,称为临时对象实质化转换(temporary materialization conversion) 。
左值到右值转换没有副作用。临时对象实质化转换没有副作用,当且仅当其中初始化临时对象时没有副作用。
临时对象实质化转换中,纯右值被实质化(materialized) 。
在求值子表达式时,按表达式具有的语义,必要时(如按相关规则判断上下文的值类别)进行值类别转换。
NPLA 还提供可能使结果具有不同的值类别的引用值提升转换(reference value lifting conversion) 。以下规则确定引用值提升转换的结果:
- 若操作数是引用值,则结果是操作数的被引用对象。
- 否则,结果是操作数。
引用值提升转换蕴含引用提升,即使用被引用对象替换操作数。
原理
为支持引用值作为一等对象(特别是未折叠的引用值),NPLA 提供比左值到右值转换更精细的引用值提升转换。
值类别转换在特定求值中适用,因此不影响构造性的规则。
特别地,列表左值(列表的引用值)不能代替列表,因此以空列表的引用作为最后一个元素的嵌套有序对是非真列表。这和 [R7RS] 约定空列表总是同一对象不同。
这种设计使语言规则更容易在局部一致,同时显著减少实现(对象内部表示)的复杂性,并有助于提升实现性能的可预测性。
注释
不同值类别表达式的转换和宿主语言中的部分标准转换类似。
根据引用值的性质,易知左值到右值转换的规约是引用值提升转换的规约的传递闭包,即:
- 若操作数是已完全折叠的引用值,则引用值提升转换等价左值到右值转换。
- 否则,有限次的引用值提升转换等价左值到右值转换。
引用值提升转换不传播不可修改属性,类似 [ISO C++] 非引用值的转换在结果中不保留源操作数中的 const
类型。
临时对象实质化可实现为空操作,因为项在先前(如返回值转换蕴含的引用值提升转换对引用项的提升操作的实现中)已被创建。
互操作可能引入不以项表达的右值而需要首先创建项。
除非另行指定:
原理
类似宿主语言规则,并非所有上下文都需要转换。类似地,宿主语言的操作符(括函数调用的第一个子表达式)可直接使用左值而不需要转换。但和宿主语言不同,因为多重引用,不确定次数的连续的转换结果不同。因此除了上下文要求,有必要约定默认仅转换一次,而非确保转换结果到右值。
必要时,具体操作仍可指定不同的规则。
值类别和左值到右值转换在一些上下文的行为类似箱和自动拆箱,约定存在默认转换并不利于维护简单性:
- 特别地,和宿主语言不同,函数不包含充分的信息(参数类型)推断是否接受左值操作数,因此在不提供针对函数的重载(overloading) 一般机制的前提下,本机实现不能预知输入的操作数是否是左值,通常需分别支持左值和右值的操作数。
- 即便提供重载,仍然较单一的值类别更复杂。
但 NPLA 的设计中,值类别转换已被通过正确反映需求的存储和对象模型的设计隐含在项的内部性质中,因此不是可选的。
而考虑统一性,对存储和对象模型的设计,用户自行的实现仍要求这些设施(尽管更困难)。
关于箱和自动装箱,参见 YSLib 项目文档 doc/NPL.txt
。
返回值转换(return value conversion) 是一次引用值提升转换和可选的一次临时对象实质化转换的顺序复合。
返回值转换用于在对象语言中确定函数调用的返回值可包含函数体的求值结果到返回值的转换。
引用值作为间接值,适用局部间接值安全保证。在返回值转换上下文中确定函数返回值的实质化转换上下文的部分操作消除引用值,即返回值转换,是这种情形的主要实例。
这可约束作为间接值的完全折叠的引用值不逃逸(因此访问被引用对象的值可不超出指向对象的存储期),而保证只考虑项可能是引用值时的内存安全。
除非证明不需要临时对象,返回值转换中初始化临时对象作为返回值的项对象,否则临时对象被复制消除。是否存在复制消除是未指定行为。
不论是否存在返回值转换,返回值的项对象来自返回的右值关联的临时对象实质化转换。这可能在返回值转换蕴含的项提升操作或之前的求值规约中蕴含。
注释
返回值转换不保证未折叠的引用值在消除引用值后的结果不逃逸。
为确保内存安全,程序仍需要保证被引用的对象的间接引用的对象生存期结束后,不能访问间接引用的对象。
其它间接值的内存安全需要另行保证。
是否需要返回值转换由实质化转换上下文中的被调用的函数而非上下文是否需要使用右值决定,无关被转换的表达式是否是左值,因此返回值转换不是左值到右值转换。
当前未实现是否需要临时对象的证明。
特定的 NPLA 非一等对象是临时对象(temporary object) 。
NPLA 允许(但不要求对象语言支持)一等对象构成的表达式通过特定的求值,在中间结果中蕴含这种非一等对象。
注释 这样的非一等对象不在源语言中可见,一般仅用于互操作。
临时对象的子对象不是临时对象。
NPLA 对象语言在特定的上下文引入其它临时对象,包括:
原理
为简化规约和互操作机制的设计,和 [ISO C++17] 不同,引入临时对象不包括延迟初始化或异常对象的创建。
关于临时对象的子对象的规则,参见绑定临时对象中的原理。
注释
关于临时对象的存储和所有权,参见求值和对象所有权。
关于临时对象的表示,参见临时对象的表示。
引入临时对象的一些上下文的和宿主语言类似。
可具有(但不保证具有)临时对象实质化转换的上下文包括:
注释
一般地,被绑定为引用值的变量在活动调用关联的环境分配临时对象。此时,对象被调用表达式的项独占所有权,同时被绑定的环境独占资源所有权,并实现复制消除。
在不具有转换时,优化的实现可能消除函数调用(内联(inline) 展开)而不分配关联的环境,把临时对象分配到其它环境或者语言不保证可见的存储(如 CPU 寄存器)中,并同时实现复制消除。
临时对象实质化转换引入临时对象的规则和 [ISO C++17] 不同:
- 不论表达式是否作为子表达式使其值被使用(未使用的情形对应 [ISO C++] 中的 discarded-value expression ),都允许存在临时对象。
- 要求复制消除而避免特定对象的初始化。
返回值转换可引入实质化的临时对象,其中可能转移求值的中间结果;否则,对象被复制。
此处被转移对象符合求值和对象所有权规则中的临时对象的定义,但除非另行指定,被转移的对象不在对象语言中可被访问。
仅在对象被复制且复制具有副作用时,返回值转换具有等价复制的副作用。
NPLA 要求特定上下文中的复制消除(copy elision) ,排除复制或转移操作且保证被消除操作的源和目的对象的同一性。
复制消除仅在以下转换上下文中被要求,即直接使用被转换的源表达式中的对象作为实质化的对象而不初始化新的临时对象:
非本机实现函数的函数体内指定的返回值不属于上述的确定返回值的上下文,但也不要求被复制消除。
实现仍可根据当前环境来判断是否在允许消除对象复制的上下文中,而进行复制消除。
复制消除不在被初始化对象以外引入新的对象语言可见的对象。
原理
为维护语言规则的简单性和使用这些规则的程序的行为的易预测性,NPLA 的复制消除限于临时对象的消除。
在完成实质化转换前的不完整的求值规约中的临时对象在逻辑上不需要作为一等对象存在,但纯右值作为对象表示中的子项,随纯右值在宿主语言中作为对象存在,以允许互操作。
复制消除的目的 [ISO C++17] 类似。同时,提供语言支持也允许更简单地实现 C++ 互操作性。
和 [ISO C++17] 不同的一些要求可简化语言规则和实现,例如:
- 不区分求值结果是否被作为返回值或求值是否为常量表达式。
- 非本机实现函数的规则不要求
return
语句中的特定的表达式,而不需要依赖特定上下文的语法性质。 - 同时,NPLA 不限制对象的类型([ISO C++17] 则要求特定的 C++ 类类型)。
注释
当前未实现按当前环境判断是否在允许消除对象复制的上下文中进行复制消除。
在使用纯右值初始化引用值时,扩展(extend) 源表达式的项对象的生存期使之比其它规则决定的生存期延长。
这和初始化非引用值类似,但实现需区分是否初始化的是延长生存期的临时对象,以确保之后能区分引用值初始化时是否按引用传递。
若实质化转换上下文支持绑定临时对象,按引用绑定(即绑定初始化使用按引用传递)的被绑定对象是临时对象。
引入引用值的形式参数需要满足的要求由引入绑定的操作或派生实现指定。
原理
绑定临时对象时指定临时对象属性允许区分通过引用绑定延长生存期的临时对象和非引用绑定的对象。
一般地,表达式中的纯右值(非引用值)被绑定为临时对象,即被绑定的对象在初始化后具有临时对象属性。
这对应宿主语言中的转发引用参数(如 std::forward
)中的情形:
- 若模板参数
P
对应转发引用函数参数P&&
,其中P
是对象或对象的右值引用类型,保留从实际参数推导(deduce) 得到的但不是实际参数类型的信息。 - 没有绑定临时对象属性的对象则同一般的非引用类型的对象类型参数(非转发引用)。
P
在宿主语言中通过值类别推断,但不表示值类别。
类似宿主语言,这种操作数表达式的值类别以外的形式是一种类型推断。因为推断规则和宿主语言的类型推导(type deducing) 相似,这种上下文可支持类似宿主语言的参数转发。但和宿主语言的 std::forward
不同,此处推断的右值除了是消亡值外,也可以是纯右值。
临时对象属性在绑定特定形式的参数时具有和 P
编码的附加信息类似的作用:
带有临时对象属性的引用值和临时对象的引用值不同。特别地,作为引用属性值的临时对象属性允许在运行时作为对象的元数据访问以及跟随对象被跨过程传递,这无法被宿主语言支持,因为 P
表示的静态类型信息不在函数外可用,仅在模板的类型参数 P
中而不在运行时可访问的元数据中(事实上,也不在对象的动态类型中)保留。关于其应用,参见进一步讨论。
注释
因为宿主语言的引用折叠,以上 P
和 P&&
一致。
被绑定的这些对象可作为临时对象引用关联的被引用对象。
另见绑定操作。
作为一等对象的临时对象和其它一等对象表示方式一致。
非一等对象临时对象包括:
对临时标签对象决定的非一等对象,去除临时对象标签后,应具有一等对象表示。
关于一等对象表示,参见 YSLib 项目文档 doc/NPL.txt
。
原理
至少在逻辑上,临时对象作为对象语言中不可见的对象和一等对象相同的宿主类型(即项)作为内部表示。因此,区分其内部表示并非通过宿主语言中的类型,而需通过运行时性质确定。
一些表示可能仅出现在临时对象中,而不是合法的一等对象表示。实现可据此进行一定运行时检查,以排除互操作或者错误实现中的误用。
对临时对象标签决定的非一等对象和一等对象表现之间的要求简化实现的一些操作,使实质化不需依赖另行分配的资源。
临时对象的子对象不是临时对象,简化对临时对象的一些操作,也减少可能使临时对象标签扩散(到非预期的上下文影响一等对象表示)而误用。
关于被绑定对象的规则,参见绑定临时对象中的原理。限制标签的使用范围以使之不和其它表示冲突。
关于实现中项的宿主类型和简化实现的例子,参见 YSLib 项目文档 doc/NPL.txt
。
注释
直接构成项的非一等对象可以是通过源代码中的外部表示经翻译变换得到的具有内部表示的数据结构的非一等对象,参见上述实现中项的宿主类型。
NPLA 的类型系统使用隐式类型;默认使用潜在类型,保证表达式的值具有类型。
空求值的求值结果要求未求值的合式的表达式应具有和语法分析的实现的输出兼容的类型。
实现对特定的上下文的表达式可使用类型推断。由此确定的类型类似宿主语言的表达式的类型。
表达式具有值类别。值类别的指派规则作为定型规则是类型系统的一部分。但除非另行指定,值类别和 NPLA 及派生语言规则中描述的表达式的类型正交。
关于语法分析的实现和其中处理的类型,参见 YSLib 项目文档 doc/NPL.txt
。
注释
类型系统和 Scheme 及 Kernel 语言类似;除了表达式具有值类别这点和 Scheme 和 Kernel 不同而类似宿主语言。
表达式的类型和 [R7RS] 的 expression type 无关,后者是语法形式的约定(在 [R5RS] 和 [R7RS] 中称为 form );因为存在合并子作为一等对象的类型,不需要这种约定。
NPLA 中值类别和表达式的类型正交,这类似宿主语言。这简化了相关类型规则的描述。
和宿主语言不同,NPLA 子表达式的求值顺序可被不同的函数(特别允许显式指定对特定操作数求值的操作子)中的求值调整,不需要特别约定。
NPLA 不存在宿主语言意义上的完全表达式,但在按宿主语言规则判断生存期时,使用本机实现的函数合并视同宿主语言的完全表达式,其本机函数调用不引起函数内创建的对象的生存期被延长。
临时对象的生存期同时约束隐含的隐式宿主函数调用(如复制构造)。
为保证求值表达式取得的临时对象的内存安全,函数合并同时满足以下规则:
- 操作符和未被求值的操作数的直接或间接子表达式关联的对象以及求值操作数的子表达式引入的临时对象的生存期结束的作用应不后序于活动调用结束。
- 生存期起始和结束的顺序被确定(determined) 时,和对应所在的表达求值之间的先序关系同构;否则,其顺序满足非决定性有序关系。
注释
生存期的顺序约束确保引入临时对象时,其生存期不会任意地被扩展而超过函数合并的求值。
具体操作可在以上约束下指定被求值的操作数可能引入的临时对象的生存期。
NPLA 表达式求值规约中最后一个规约步骤中的上下文是尾上下文。
尾上下文在 NPLA 中可满足一些附加的性质。
尾上下文涉及的存储在特定情况下满足调用消耗的空间有上界(即空间复杂度 O(1) )。
满足这种情况下的规约称为真尾规约(proper tail reduction) 。
在尾上下文规约的调用是尾调用(tail call) 。
以真尾规约的实现尾调用允许具有不限定数量的(unbounded) 活动调用,称为 PTC(proper tail call ,真尾调用)。
PTC 占用活动记录满足真尾规约的上界的要求。
当宿主语言提供函数调用支持 PTC 时,可直接使用宿主语言的 PTC 调用,否则,需要使用其它替代实现机制确保 PTC 。
非对象语言的调用的上下文中,若被调用时间接使用,也仍需要保证 PTC 。
PTC 确保仅有一个活动的调用。不满足 PTC 的情形下,语言没有提供用户访问非活动记录帧资源的手段,因此可以认为是资源泄漏。但为简化语义规则,NPLA 不要求避免相关的弱内存泄漏。
NPLA 不添加保证活动记录帧中保存引用,销毁活动记录的帧可能影响环境中的变量生存期而改变语义。
注释 NPL 不保证一般对象存在引用。
因此,除非依赖本节中以下的规则,NPLA 不保证提供 PTC 支持;实现更一般的 PTC 依赖派生实现定义的附加规则。
除非另行指定,NPLA 要求至少在被求值算法中蕴含的函数应用的求值支持 PTC 。
为满足 PTC ,在生存期附加约定的基础上,尾上下文内可以确定并调整对象生存期结束时机:
- 作为临时对象的合并子及其参数可以延长生存期至多到尾上下文结束。
- 被证明不再需要之后引用的对象,或未被绑定到活动记录上的项中的对象,可以缩短生存期。
- 被延长生存期的对象生存期起始和结束的相对顺序保持不变。
- 被缩短生存期的不同对象生存期结束的相对顺序保持不变。
推论:被缩短生存期和延长生存期的对象的生存期结束的相对顺序保持不变。这由没有被调整生存期的对象与被调整生存期对象之间的生存期结束的顺序关系的传递性保证。
延长临时对象生存期和宿主语言中允许扩展非完全表达式内的临时对象的效果类似,但条件不同。
原理
要求 PTC 主要用例是支持 PTR 。相对 PTR ,PTC 更具有一般性,也适合对象语言可观察行为以外的普遍性质。
注释
以上规则中被调整生存期的对象一般仅是参数和函数体内创建的对象。因此,不保证理论上允许的尾上下文的都满足 PTC 。一个例子是合并子中可以保存动态环境,这个环境可能被递归的调用引用,而无法提前释放。
理论上 PTC 不要求延长生存期,仅要求特定情形下缩短生存期,且其它情形被释放的对象生存期不延长到尾上下文外。
允许延长生存期是生存期附加约定的结果。
PTC 的活动记录性质也在一般的递归规约时体现,被称为 PTR(proper tail recursion ,真尾递归)。
和 PTC 不同,PTR 要求的递归规约不一定是对象语言中的调用,以 PTR 描述时仅强调递归,不考虑尾上下文的适用性。
通过特定的保持语义等价的变换,对象语言可要求尾上下文作用于函数调用以外的上下文中(例如非函数合并的语法上下文)使用真尾规约实现。
除非派生实现另行指定,NPLA 对象语言不指定使函数调用以外的上下文作为尾上下文的要求;函数调用以外的尾上下文规约的仅可能用于实现的元语言中的管理规约;非管理规约的真尾规约都用于尾调用。
此时,PTR 等价被递归调用的 PTC 。但由于支持 PTC 在非递归规约情形时也影响语言实现的一般构造,所以描述要求时一般不以 PTR 代替 PTC 或真尾规约。
关于 PTR 在 Scheme 为基础的形式模型,参见 [Cl98] 。
PTR 的一个更激进的实现优化方式是 evlis tail recursion ,参见以下文献和参考资料:
因为 NPLA 使用链接的环境,不支持实现其中更激进的 safe-for-space 保证。
原理
支持 PTR 使重入的函数调用保持较小的空间开销。这允许使用递归的函数调用代替尾上下文中特设的循环(loop) 和迭代语法实现等效的算法功能,满足简单性和通用性。
与控制状态和支持一等状态的实现之间具有的偶然耦合不同,使用支持 PTR 的递归函数调用代替循环的耦合可以是足够必要的:它排除了特设的循环语法的需要,同时也能满足实现自身的简单性,也因此可能更高效。
不具有 safe-for-space 保证时,实现对程序的闭包变换(closure conversion) 可能创建多余的循环引用且无法被运行时有效地分辨,而造成资源泄漏。通过明确支持这类保证的变换(如这里描述的设计 )可避免变换引起资源泄漏。
只要捕获自由变量的静态环境可被程序在创建函数时明确指定,safe-for-space 保证就不是必须的:避免在语义规则约定之外的生存期延长和资源泄漏是用户程序的责任;NPLA 程序可精确控制对象生存期,同时应当避免循环引用。
对 safe-for-space 保证的证明(如 [Cl98] 和 Closure Conversion Is Safe for Space ),隐含要求和上述一等对象支持冲突的条件:环境中引用的对象总是可被复制的没有可观察行为的值。这实质上要求支持共享引用乃至可能要求一等对象都是引用。
因为使用链接的环境的要求,一般情形不支持对 safe-for-space 的变换。
即便允许类似的变换,这也仅保证不存在不可达,仍然不保证资源被及时回收——全局机制可能具有不确定的延迟而造成的实时资源泄漏。
TCO(Tail Call Optimization ,尾调用优化)是在以尾上下文规约时,允许减少修改规约状态的优化。
一般地,TCO 可重新排列规约过程中的被语义允许调整的副作用和其它不影响可观察行为的状态的调用,减小空间开销。
TCO 的这种性质可以在宿主语言不支持 PTC 时用于实现对象语言的 PTC 。
注释
关于 TCO 和 PTC 的差异,另见这里。
C++ 不要求实现支持 PTC ,也不保证支持 TCO 。因此,对象语言的 PTC 要求显式的 TCO 实现。
为可移植地支持 TCO ,NPLA 不依赖宿主语言中不可移植的互操作的活动记录(通常是体系结构相关的栈)。
注释
尾调用可避免尾上下文中非嵌套调用安全的情形的宿主语言实现的未定义行为,但不保证非尾上下文中具有类似的性质。
TCO 包括以下形式:
- 静态 TCO :实现时替换宿主语言中不保证满足 PTC 的构造为满足 PTC 的构造。
- 动态 TCO :运行时调整直接或间接表示对象语言构造的数据结构和状态,使状态占用的空间复杂度满足 PTC 要求。
静态 TCO 也适合非对象语言的调用的上下文。
不依赖宿主语言特性的静态 TCO 包括以下形式:
- 替换宿主语言实现中的不保证满足 PTC 的递归调用为满足 PTC 的结构(如循环结构),包括直接编码和自动的变换(transformation) ,称为宿主(host) TCO 。
- 替换不满足 PTC 的对象语言原语为满足 PTC 的表达形式,称为目标(target) TCO 。
不依赖宿主语言特性的动态 TCO 包括以下形式:
- 通过合并不同活动调用中活动记录占用的冗余状态,减少宿主语言的活动调用同时占用的总空间,称为 TCM(Tail Call Merging ,尾调用合并)。
- 引入具有便于操作控制作用的构造,同时作为一些其它优化的基础,以消除部分活动记录状态的分配,称为 TCE(Tail Call Elimination ,尾调用消除)。
注释
本节的内容不影响对象语言的语义,但可能影响互操作的接口兼容性和实现质量。
关于对实现的具体影响,参见 YSLib 项目文档 doc/NPL.txt
。
NPLA 数学功能(模块 NPLAMath )提供数学功能和相关支持。
关于 NPLA 数学功能的规格说明的其它部分,参见 YSLib 项目文档 doc/NPL.txt
。
NPLA 数值是 NPLA 对数学意义上的数(number) 的建模。
被建模的数是 NPLA 数值的真值(true value) 。
NPLA 数值的集合到真值的集合的映射是满射;除此之外,也存在不被 NPLA 数值建模的数,这些数可能被 NPLAMath 未来的版本支持作为真值。
除非另行指定,NPLA 数值的行为由对应的真值的数学含义决定。
基于宿主语言的类型系统,NPLA 支持以下按数值范围从小到大排列的本机整数和浮点数作为宿主类型:
signed char
unsigned char
signed short
unsigned short
int
unsigned
long
unsigned long
long long
unsigned long long
float
double
long double
文法表示:
支持的数值类型以 <number>
表示,具有以下表示数值的子类型:
-
<complex>
:复数。 -
<real>
:实数。 -
<rational>
:有理数。 -
<integer>
:整数。
其子类型由数学定义蕴含,即以上类型中,后者依次是前者的子类型。
当前所有数值都是 <real>
,因此暂时没有针对 <number>
值是否属于 <real>
和 <complex>
的类型检查。
和数学意义上的实数不同,<real>
也包含以下可带符号(sign) 的特殊值(special value) :
- 无限大值。
- NaN(not a number) 值。
对应地,<complex>
也包含实部和/或虚部是上述特殊值的特殊值。
注释 当前所有复数都是实数,因此虚部总是 0 。
根据数值是否完全保留真值在数学上的唯一性即精确性(exactness) ,数值分为精确数(exact number) 和不精确数(inexact number) 。
精确数和对应的真值总是相等;不精确数和真值不严格相等。
有限的不精确数的偏离程度可通过实数描述,即(绝对)误差(error) 。精确数的误差恒等于 0 。
除非另行指定,特定不精确数的具体的误差是未指定的。
数值的绝对精度(precision) 是其内部表示蕴含的误差的上界的倒数。对确定使用进位制的表示,精度也指精确表示的数值位数。
数值的任意精度(arbitrary precision) 指除实现环境的可用资源(一般即存储空间)限制外,不限制精度。
注释 为支持更多数学上有意义的真值,未来可能引入其它类型来表示任意精度的整数、有理数及数学意义上的扩展(如复数和四元数)。
数值的内部表示中能以实数描述的度量应至少具有整数数量级精度,即误差不大于 1 。
精确数和不精确数在数值上可能相等,而类型不同。
宿主类型中的本机整数和浮点类型是数值类型的子类型,分别称为 fixnum 和 flonum 。这些类型在项的内部表示预期直接占据本机存储而不需要动态分配。
Fixnum 总是精确数;flonum 总是不精确数。
注释 当前实现中,所有数值是 fixnum 或 flonum 之一。两者分别是宿主的整数类型(排除字符和 bool
类型)以及浮点数类型。
Flonum 支持带符号的无限大值以及 NaN 值作为特殊值。其它值都是有限值。特殊值可能具有不唯一的内部表示,但和有限值的表示都不同。
Flonum 中可存在小的非零数,可能和其它数值不同的内部表示而更容易在计算中损失精度,即非规格化(denormalized) 数值。
整数值的数值具有整数类型。这包括所有的 fixnum ,以及 flonum 中是整数的数值。这不和宿主类型直接对应。
注释 一个 flonum 是整数,当且仅当它的值取整后结果和原值相等。这里的取整使用可使用任意的舍入。[R7RS] 对不精确数有类似的定义(仅使用 round )。
对 fixnum ,+0
和 -0
是相等的数值。Flonum 不同符号的零值在值的表示中可以不同,但在数学意义上相等,表示同一个数。实现中的其它和具体表示无关的等价谓词是否表现这种不同是未指定的。
注释
NPLA 的不同的名义数值类型的集合到宿主类型的集合的类型映射是满射,即本节指定的宿主类型总是关联至少一个能表示它的值的 NPLA 数值类型。
同时,NPLA 数值类型可以映射到其它类型,特别地,NPLA 整数的类型映射目标是宿主语言整数类型和包含整数值的非典型宿主类型的并。所以,NPLA 数值整体的类型映射不构成简单的一对多或多对多关系。
特殊值同 ISO/IEC 10967–1 (LIA–1) 定义,引用 [IEC 60559] (IEEE-754) 的具体值,仅适用于浮点数。
无限大值在数学上属于超实数(hyperreal number) ,在浮点数实现中属于扩展实数(extended real number) 。
无限大值的符号在数学意义上是必要的,因此也被要求区分。
精确性、fixnum 和 flonum 等区分同 [R6RS] ,但具体实现要求不尽相同。
NaN 不是数学意义上的数,表示特定的没有数学定义的计算结果。NaN 和任何数值比较总是不相等。
NPLA 的宿主语言支持的 NaN 值带有符号。不是所有实现都区分符号,如 [ECMAScript] 。
整数精度外的数量级精度仅在确定使用的进位制底数时和绝对精度可比较,因此常用于描述特定实现的内部表示(例如,[ISO C] 定义的浮点数精度即有效数字的位数)。但是,不比较具体大小时,有限的数量级精度和绝对精度性质可以一致,这种上下文可不区分两者。
大多数不精确数的浮点表示的高效实现使用底数 2 。
参照 [IEC 60559] ,浮点格式的无限大值和有限数值是浮点数;NaN 不是浮点数。两者统称为浮点数据(floating-point datum) 。
宿主类型中实现为 [IEC 60559] 的非规格化(denormalized) 浮点数是具有不唯一的内部表示的小的非零数值。使用 IEEE-754 2008 以来的定义,这些数是非规格(subnormal) 数。
和 [RnRK] 不同,本文档没有指定可选的模块,也没有指定精确的 ±∞ 值。
和 [RnRK] 不同,不精确数不指定边界和主值(primary value) ,NaN 值被显式提供而不是唯一的 #undefined
值,非规格数不作为 #undefined
。这同时不要求在任意的操作中检查 #undefined
值并引起错误。
尽管容易损失精度,区分不同的非规格数的数值仍然有意义。同时,也避免和 NaN 用以表示数学上未定义操作的结果(如 0 除以 0 )引起混淆。
数值相等和一般对象相等可使用不同的等价谓词。和一般对象比较不同,数值相等比较可对参数要求数值类型,否则引起错误。两者比较结果可能也不总是相同。如 Scheme 的 =
和 eqv?
以及 Kernel 的 =?
和 equal?
。
在对象语言中,数值操作是可使用数值作为算法输入的值的操作。NPLA1 提供本机 API 支持这些操作的实现。
数值操作数和非数值操作数分别是具有和不具有数值类型的操作数。
数值操作蕴含对应的数值计算,接受至少一个数值或非数值操作数,预期得到计算结果。
注释 非预期情形可引起错误。
其中,计算结果依赖影响计算结果的操作数,并依赖至少一个数值操作数。
除非另行指定:
- 在数学上有意义的前提下,数值操作同时支持以上尽可能多的数值类型的操作数。
- 数值操作对预期的数值操作数进行类型检查,失败时出错。
- 数值操作不区分数值操作数中对应的真值相等的精确数或不精确数。
- 可假定数值操作数和计算过程中不出现 SNaN(signaling NaN) (en-US) 值。
- 若作为操作数的精确数决定计算结果在数学上未定义,则引起错误。
- 不精确数计算中的舍入方式未指定。
- 若计算结果是数值,则:
- 若被计算结果依赖的任一操作数中具有 NaN 值,则依赖这个操作数的数值操作结果也是 NaN 值。
- 输出的类型的值域能表示操作结果;除操作的语义和本节的其它规则蕴含外,具体类型未指定。
- 若作为操作数的精确数决定的计算结果是不精确数表示的有限数值,则这个不精确数应是所有相同内部表示的数值中和结果的真值误差最小的数值。
- 对数学上封闭的计算,结果具有不超过所有数值操作数范围的数值类型。
- 除非不能在结果类型中表示计算结果的范围:
- 若计算结果是不精确数,则:
- 若计算结果是小于最小可唯一表示的
<real>
值,则对应的数值操作结果是不精确数 0 。 - 计算结果中真值等于 0 的数值以及 NaN 值的符号是未指定的。
- 若计算结果中无限大数值不能通过数学上有意义的方式确定符号,则对应的数值操作结果是无限大值或 NaN 之一,具体选择未指定。
- 若计算结果是小于最小可唯一表示的
- 数值的宿主类型未指定。
原理
因为典型的高效实现实现依赖外部环境对浮点数的支持,设计策略以保持互操作的便利性相关。
- NPLAMath 实现不访问 SNaN 值,也不需要访问宿主语言的浮点环境,但不假设总是使用默认浮点环境。
- 这不阻止和使用 SNaN 的本机实现的程序链接和调用,这有助于保持互操作性。
- NPLAMath 实现不保证检查访问浮点环境的副作用是否存在。若互操作需要改变浮点环境,应避免破坏实现的假设。
- 不依赖零值的符号、NaN 的符号以及 SNaN 的处理和许多宿主实现的默认情形一致而能简化一般的实现,如:
- GCC 。
- Microsoft VC++ 。
- 不要求使用 GCC 时启用
-ffloat-store
。Microsoft VC++ 默认的/fp:precise
的类似语义也不被依赖。
注释
数值操作可能允许非数值的操作数,这些操作数也可被计算结果依赖。
因为 flonum 能表示所有实数数值范围,所以实数范围以内的操作不会引入操作数以外的其它 flonum 类型。
数值操作抛出异常的要求不一定在每一个实现数值操作的 API 中蕴含,因为这些 API 不一定是数值操作的完整实现。
抛出异常和宿主环境的异常和浮点异常没有直接关联。
若数值操作指定非数值计算结果(如布尔值)或者不能表示的 NaN 值的计算结果,则即使依赖 NaN 数值操作数,也不是 NaN 值。
和 [RnRK] 不同,数值操作不支持不同的全局模式。
特定情形下,精确数可能替换计算结果中的不精确数:
- 当按数学定义能被精确表示时,计算结果可以是符合要求的任意一个类型的精确数。
- 否则,当实现能证明不精确数值足够小到不足以影响结果的表示,且存在真值相等的可用的精确数时,可使用这个精确数代替不精确数。
- 当前实现没有这类证明机制。
IEEE-754 使用渐进下溢(gradual underflow) ,使零值和相邻的非零浮点数的真值之差不会显著大于其它两个浮点数真值的差,而使小的非零浮点数之间的差不等于零。这体现了支持非规格化数的实际作用。但一般数值计算仍需要累积误差。
虽然可能影响结果,浮点数实现的内部状态(如舍入模式)的访问不被直接支持。
浮点数 0 和 0 之差的符号可能取决于舍入模式。数值操作一般不保证结果 0 的符号,但以依赖表示的形式仍可确定符号。
除满足必要的精度要求的前提外,互操作以外目的的数值的宿主类型的具体选择在维持计算正确性的意义上通常不重要,因此默认不要求指定。
对特殊值,因为 [RnRS] 只要求 .0
后缀的特殊值字面量,需保持兼容时,程序可只使用这些形式的字面量。为此,实现可使用带有 .0
后缀特殊值的数值字面量的对应的宿主类型,以减少潜在的可移植问题。
关于派生实现支持的数值字面量,参见 NPLA1 数值字面量。
支持解析的数值以字符串作为外部表示。作为字面量时,构成数值字面量的词法。
数值的外部表示和内部表示应支持往返(round-trip) 转换,即转换的内部或者外部表示输出可被输入接受。
往返转换中,精确数转换保持任意(无限)精度;不精确数经有限次转换不继续损失精度。
注释 即便损失精度,也应总是满足结果至少不低于整数精度。
支持的外部表示和对应的含义具体包括:
- 数值的外部表示中起始的一个
+
或-
字符指定符号。- 这可能是可选的。若符合规则的数值字面量没有指定符号,则隐含为
+
。 - 注释 不精确数可能在内部表示支持不同符号的零值。
- 这可能是可选的。若符合规则的数值字面量没有指定符号,则隐含为
- 以下优先匹配较长的模式。
- 匹配正则表达式
(+|-)?[0-9]+
:十进制整数值。 - 匹配正则表达式
(+|-)?[0-9]+\.[0-9]*
或(+|-)?[0-9]+(\.[0-9]*)?(E|e|S|s|F|f|D|d|L|l)(+|-)?[0-9]+
:十进制不精确数数值。- 不精确数数值字面量的解析使用未指定的浮点数舍入模式,其误差不大于最后一个在规格化范围内表示的十进制小数位为 1 时的绝对值的真值大小。
- 解析不精确数外部表示得到的真值和内部表示可具有误差。
- 注释 误差和具体宿主语言支持相关,通常以任意可能符合宿主语言要求的舍入模式下的最大值计。
- 第一种形式是直接记法。
- 第二种形式是科学记数法(scientific notation) ,在指示指数的指数字母前后匹配的数字序列分别是有效数字(significand) 和指数(exponent) 。
- 指数字母表示作为 flonum 的不同精度:
-
E
或e
:默认精度。 -
S
或s
:短(short) 精度。 -
F
或f
:单精度(float) 。 -
D
或D
:双精度(double) 。 -
L
或l
:长精度(long) 。 - 同组的字母含义等价。以上精度中,默认精度不低于双精度,其它精度依次不低于之前的一个。
-
- 精度可影响内部存储的宿主类型。
- 指数字母表示作为 flonum 的不同精度:
- 若字面量指定的数值小于或大于使用的类型的数值表示范围,则值为对应类型具有相同符号的零值或无限大值。
- 匹配正则表达式
(+|-)(inf|nan)\.(0|f|t)
:带符号的 flonum 特殊值。- 其中,
inf
指定无限大值,nan
指定 NaN 值。 - 后缀指定精度:
-
0
:默认精度。 -
f
:单精度。 -
t
:扩展(extended) 精度。 - 以上精度中,默认精度不低于双精度,扩展精度不低于默认精度。
-
- 其中,
除非派生实现另行指定,以上要求外的数值的子类型和内部表示未指定。
原理
浮点数解析存在不同精度的算法。
若以二进制浮点数和经过舍入的十进制表示相互转换不损失精度为前提,宿主语言的 std::numeric_limits
的 max_digits10
位十进制数字足够表示。
(对 [IEC 60559] 的二进制浮点数情形的证明参见这里的 Theorem 15 。)
但是,对任意有效输入的结果误差都不大于 1 ULP(unit in the last place) 的不经舍入的值完全精确值(full precision) 的精确解析算法,对实现的要求较高,且性能可能明显较低,故不作要求。
(对 [IEC 60559] 的二进制浮点数的情形,需要数十倍的中间存储,参见这里。)
和宿主语言的 std::strtod
不同,允许使用宿主语言中的任意浮点数舍入模式,而不要求不同浮点数舍入模式下的结果一致性。
浮点数精度的 float
和 double
在典型实现中的内部表示格式同 [IEC 60559] 的二进制的单精度和双精度浮点数。
注释
串模式 (+|-)
表示带有可选前缀符号(仅限一个),影响值的数值。
同 klisp 而不同于 [RnRS] 的字面量词法,小数点不能出现在词素中符号以外的第一个字符;但 klisp 的 string->number
没有这个限制。
同 [RnRS] 而不同于 klisp(两者包括字面量词法和 string->number
),小数点允许出现在词素的结尾。
当前不精确数数值都具有宿主类型 double
。即便 long double
可能具有更大的数值范围,也不能通过解析数值表示直接取得。
类似地,[Racket] 默认不启用 extflonum。关于数值操作也类似,参见数值操作约定。
当前允许在宿主值不能完全存储不精确数的字面量数字时,解析十进制不精确数字面量存储的值可能和字面量的数值的真值之间具有超过 1ULP 的误差。这可能影响和精确数之间的比较。
当前实现使用四舍五入。
关于宿主语言中 std::strtod
(同 [ISO C] 标准库的 strtod
)舍入要求的一些问题,参见:
数值字面量的词法同 [RnRS] 的一个子集。
[RnRS] 指出实现可能允许用户修改不同的默认精度。这指出精度不是固定的,但不是实现要求。 [RnRK] 的有限数的对应子集接近 [RnRS] 的设计,但没有明确指定字面量词法规则。其中对精度的表述略有不同:
- 没有指定大写字母。
- 指定
s
、f
、d
和l
的精度递增,没有显式允许不同的精度映射到相同的内部格式。 - 没有显式允许用户指定的默认精度。
但是,SINK 和 klisp 实际上都不符合前两点,而更符合 Scheme 的实现。[RnRK] 在此可能不完善或表述有误。
当前 klisp 的实现不允许 E
和 e
之前没有小数点,且在存在 E
或 e
时省略之后的指数的任意部分,和 SINK 以及 [RnRS] 都不同。本设计遵循后者。
当前 NPLAMath 精度对应的宿主类型指派如下:
- 非特殊值:
-
e
:同d
。 -
s
:同f
。 -
f
:float
。 -
d
:double
。 -
l
:long double
。
-
- 特殊值:
-
0
:double
。 -
f
:float
。 -
t
:long double
。
-
指定特殊值的精度的词素语法兼容 [Racket] 。
NPL 是独立设计的,但其派生语言和其它一些语言有类似之处;这些语言和 NPL 方言之间并不具有派生关系。但为简化描述,部分地引用这些现有语言规范中的描述,仅强调其中的不同。
NPLA1 符合 NPL 和 NPLA 的语言规则,其实现环境还应提供本章起的其它程序接口。
互操作中的一些接口处理的值可约定宿主类型。但这些类型不一定在对象语言层次上稳定,可能在之后的版本变化。稳定性由具体实现提供的附加规则(若存在)保证。
NPLA1 和 Kernel 语言(即 [RnRK] )的特性设计(如 vau和作为一等对象的环境表达过程抽象)有很多类似之处,因此许多概念是通用的;但从设计哲学到本章介绍的各个细节(如默认求值规则)都存在深刻的差异。
部分名称指定的操作和 [RnRS] 或 klisp 指定的类似。
以下章节主要介绍和 Kernel 约定不同的设计。各节的通用约定不再在之后的各个接口单独说明。
NPLA1 仅使用宿主语言的类型和值作为在对象语言可表达的状态。
在 NPLA 的基础上,NPLA1 要求对象语言支持以一等对象作为表达式并被求值。
类型等价性基于类型映射及其实现,由 [ISO C++] 的语义规则定义。
值等价性由宿主环境的 ==
表达式的结果定义。
除非另行指定,所有类型的外部表示都是允许从作为内部表示的项节点确定的同宿主类型的空字符结尾的字符串(即 [ISO C++] 的 NTCTS )。
关于作为表达式的求值和类型映射的实现,参见 YSLib 项目文档 doc/NPL.txt
。
部分名称是保留名称:含有 $$
的名称保留给宿主交互使用;含有 __
的名称保留给 NPLA1 实现。
在 NPLA 规则的基础上,在内部表示中显式使用保留给实现的标识符的程序行为未定义。
注释 这包含在源代码以外的中间表示使用的情形,但不包含作为用户输入的数据。
基本规则参见 NPLA 互操作支持。
非 NPLA1 实现提供的类型的宿主 ==
操作不要求支持嵌套调用安全。
作为 NPLA1 嵌套调用安全的约定的扩展,若存在 == 操作不支持嵌套调用安全的类型,具体类型由派生实现的定义。
对象语言操作和互操作不修改对象语言中已经可见的一等环境的父环境。
原理
NPLA1 中提供的类型仍需要支持嵌套调用安全,以满足嵌套调用安全的约定中的要求。
关于 NPLA1 嵌套调用安全的具体约定和其它实现原理,参见 YSLib 项目文档 doc/NPL.txt
。
避免修改已在对象语言可访问的一定对象的父环境符合同 [RnRK] 的环境封装性对对象语言的要求。这允许实现假定仅在有限的上下文中父环境可修改,而减少优化实现的难度。
本章指定 NPLA1 对象语言的核心语言特性。包含库特性的其它设计参见 NPLA1 参照实现环境。
原理
一般的语言能支持不同实现形式的库,包括源程序和其它无法判断是否和特定源程序关联的翻译后的程序。
复用这些程序时,可能需要根据不同的形式而分别处理:源代码被读取和求值而加载,而其它格式的翻译形式可能直接映射存储后经特定的检查即被加载。
但是在可复用的意义上,这些不同的形式是一致的,都视为库。
注释
和 [RnRK] 不同,库不限定其实现形式。[RnRK] 指定的库实质上是可使用对象语言派生实现的库。
典型的静态语言不保证程序执行时能对源程序进行翻译,因此加载程序的限制通常更大,可能无法处理源程序形式的库而首先需要分离翻译为其它格式。NPL 一般不具有这个限制。
关于对象语言的派生实现,参见 YSLib 项目文档 doc/NPL.txt
。
基于 NPLA 整体约定,由 NPL-EMA ,NPLA 的实现不假定存在多线程执行环境。
但是,宿主语言可支持多线程环境执行,可引起宿主语言的未定义行为。
作为 NPLA 的派生,NPLA1 对象语言程序也具有相同的性质,除非另行指定需要和外部环境交互的特定操作,不需要假定 NPLA1 引入存在多线程执行环境。
NPLA1 实现可提供和实现环境或具体 NPLA 对象关联的附加的资源,用于提供程序运行时可得到的附加信息,如源代码位置。
是否存在这些附加元数据(extra metadata) 和附加元数据的具体内容可影响特定的行为。
注释 如符合诊断中要求的实现的具体行为。
这些影响是未指定的,但除 NPLA1 程序直接依赖具体数据而进行的操作外,不应影响程序的其它语义(例如,引起程序终止)。
本章中除循环引用的限制外,不支持的特性可能会在之后的实现中扩展并支持。
NPLA1 对象语言程序中的未定义行为包括 NPLA 未定义行为和以下扩展 NPLA 未定义行为:
- 特定情形下访问被修改的环境中绑定的对象。
- 特定情形下违反对环境的使用要求。
- 注释 参见环境的稳定性。
- 特定情形下违反对外部实现环境假定的使用要求。
- 注释 参见模块的初始化和加载。注意虽然可能通过环境实现,但这在接口的意义上独立于环境的稳定性。
- NPLA1 库约定的未定义行为。
- 注释 有些未定义行为不需要被显式约定,例如不支持的并发访问可引起宿主语言的未定义行为,属于 NPLA 未定义行为。
派生语言可约定其它未定义行为。
原理
扩展 NPLA 未定义行为可提供更严格的要求使实现更简化。
关于环境的一些未定义行为可视为违反内存安全,而不需要单独实现。
为描述对象语言规则和程序接口,本节约定文法形式。
注释 这仅用于描述接口,不依赖 NPL 语言的基本文法。
元语言文法:
<left-constraint-bound> ::= <
<right-constraint-bound> ::= >
<constraint> ::= <left-constraint-bound><constraint-name><right-constraint-bound>
<prefix> ::= .
<suffix> ::= ? | ...
<trailing-constraint> ::= <constraint> | <constraint><suffix> | <prefix><trailing-constraint>
<sequence> ::= <constraint>*<trailing-constraint>
<left-sequence-bound> ::= (
<right-sequence-bound> ::= )
<form> := <sequence> | <left-sequence-bound><sequence><right-sequence-bound>
规约操作中项的约束通过具有同类名称(即 <constraint-name>
元素)的前后以 <
和 >
作为边界的文法元素表示,即 <constraint>
文法元素。
为区分同类约束的不同项,约束的名称后(在 >
之前)的可带有以 1 起始的正整数序数。除非另行指定,这些序数仅用于区分不同的同类约束项,无其它附加含义。
本节描述的项是被用于求值(参见求值算法)的项或它们的直接文法组合。前者应能涵盖原子表达式、其求值结果以及预期在对象语言中实现对象语言求值算法所需的 NPLA1 用户程序构造。
库可参照本节的方式约定通过项的文法,以支持仅在特定库使用的操作数。
除非另行指定,本节的对应要求同时适用于本节中和这些库中引入的项。
描述接口的元语言的文法具有以下含义:
- 表达操作的文法是
<sequence>
,表示匹配项的文法元素<constraint>
或<trailing-constraint>
的序列。 - 当
<sequence>
不存在具有非空<prefix>
的<trailing-constraint>
时,序列是真列表。 -
.
仅在序列的最后一个文法元素中出现,表示后继的项和可选的文法元素后缀是有序对的最后一个元素。 - 文法元素后缀的含义如下:
-
...
:Kleene 星号,重复之前修饰的<constraint>
0 次或多次。 -
+
:重复之前修饰的<constraint>
1 次或多次。 -
?
:重复之前修饰的<constraint>
0 次或 1 次。
-
注释
...
一般在结尾出现,表示元素构成列表。
和 [RnRK] 不同,不使用 .
分隔有序对,不使用元素名称的复数表示列表。
指定具名的函数的文法中,第一项以符号值的形式在所在的环境中提供,指定求值结果指称为合并子的函数的名称;其后指定的文法中不同的元素对应合并子的操作数或其被作为调用时的形式参数树的子项。
除非另行指定,在操作数可能是左值时,仅当对应以 ...
或 ?
形式中最后的一项(若存在)时,支持匹配作为被引用对象的有序对的非前缀元素不是空列表的引用值的情形。
名义不同的约束可能蕴含相同的检查。
除非另行指定,应用子的操作数的约束也适用其底层合并子。
注释 这意味着意味着按求值算法,被求值的应用子的函数合并对象不能是非真列表。特别地,...
形式的结尾序列一般被要求是真列表,除非是其底层操作子被求值且具有存在 .
前缀的最后一个文法元素。
文法形式上,使用本节约定指定应用子的操作数时,指定表达式形式的求值结果。
注释 这和 [RnRK] 和 [Shu10] 中的斜体标识符的标记不同,但含义(表示语义变量(semantic variable) )和效果实质相同。
操作数可能是左值或右值,按具体操作的需要,在必要时可被转换。
除可能具有的子类型关系,本节约定的不同类型的操作数构成的集合之间不相交。一般规则参见类型分类。
根据是否可作为操作子中指定不被求值的函数参数,本节的操作数及其子项分为未求值的操作数和求值得到的操作数。
原理
约束可用于区分特定的含义,但不直接指定和具体的检查对应,以便被实现优化,例如合并名义不同的检查。
文法形势的匹配应避免歧义。
注释
- 仅结尾序列支持匹配以空列表以外的值的引用值作为非前缀元素的有序对操作数左值的被引用对象中的非前缀元素子对象。
- 非结尾序列的元素因计算前缀元素数而被要求在同一个对象的前缀元素中。
- 作为操作数被绑定时,若元素是引用值:
- 若有序对操作数的非前缀元素是空列表的引用值,则有序对操作数构成列表。
- 关于操作数匹配的规则避免匹配操作数序列时对文法元素的对应关系可能具有歧义。
未求值的操作数的文法约定如下:
-
<symbol>
:符号。-
注释 内部使用和
<string>
一一对应的表示,不提供符号和外部表示的其它映射关系。
-
注释 内部使用和
-
<symbols>
:元素为<symbol>
的列表,形式为(<symbol>...)
。 -
<eformal>
:表示可选提供的环境名称的<symbol>
或#ignore
,或这些值的引用值。- 注释 通常为动态环境。
-
<expression>
:待求值的表达式。 -
<expressions>
:形式为<expression>...
的待求值形式。- 求值时,
<expressions>
被作为单一表达式(即视为求值(<expression>...)
)。
- 求值时,
-
<binding>
:绑定列表的元素,形式为<symbol> <body>
,用于指定被求值的表达式和绑定参数的符号值。- 和 Kernel 不同,
<symbol>
后不要求是整个<expression>
。
- 和 Kernel 不同,
-
<binding>
绑定列表,形式为<symbol> <expressions>
,用于指定被求值的表达式和绑定参数的符号值。 -
<bindings>
:绑定列表,即元素为<binding>
的列表,形式为(<binding>...)
。 -
<body>
: 出现在元素的结尾<expressions>
形式。- 注释 一般用于函数体等替换求值的目标。
-
<expression-sequence>
:同<expression>...
但蕴含顺序求值其中的子项。- 求值
<expression-sequence>
的结果是求值其最后一个子表达式(若存在)的结果,或当不存在子表达式时为未指定值。
- 求值
-
<consequent>
:同<expression>
,仅用于<test>
求值结果经左值到右值转换不为#f
时。 -
<alternative>
:同<expression>
,仅用于<test>
求值结果经左值到右值转换为#f
时。 -
<ptree>
:形式参数树,是包含符号值或#ignore
及其它形式参数树构成的 DAG 的表达式。- 语法要求由上下文无关文法描述:
<ptree> ::= <symbol> | #ignore | () | (<ptree>...)
。
- 语法要求由上下文无关文法描述:
-
<definiend>
:被绑定项的目标的<ptree>
。 -
<formals>
:作为形式参数的<ptree>
。同<definiend>
但允许派生实现定义更多检查。 -
<clauses>
:元素为条件分支的列表,形式为(<test> <body>)...
。 -
<variable>
:变量。用于表示被声明的名称。- 同
<symbol>
,其中的处理与作为非列表的<formals>
相同。
- 同
关于 eval
,参见 YSLib 项目文档 doc/NPL.txt
。
原理
以 <expressions>
代替 <expression>
可避免语法中要求过多的括号及 eval
等求值形式中显式构造列表的需要。
因为 <body>
存在元素的结尾,明确元素中的其它词法元素后即可自然确定边界。
<body>
可以是多个表达式的词法组合,允许具体使用时不需要附加括号即可实现整体求值。
特别地,作为其它 <body>
嵌套的 <body>
实例在这种情况下,可以更有效地减少嵌套一层以上的括号。
<body>
整体求值的一个必要条件:构成 <body>
的表达式不被以其它方式分别求值,如蕴含顺序求值。
注释
和 [RnRK] 不同,<body>
可以是多个表达式的词法组合。
尽管 <body>
不保证可直接构成一个表达式(而是构成某个表达式的所在元素中的多个子表达式),一般仍被作为一个整体求值。
被整体求值时,这些表达式被视为某个假想的表达式,这个表达式包含被整体求值的表达式作为子表达式。
若 <body>
存在超过一个子表达式,按求值算法的 NPLA1 规范求值算法步骤,表达式分别作为合并子和之后的参数。
若 <body>
不存在子表达式,则结果是 ()
而不是 #inert
。这和 [RnRK] 的经重定义而隐含 $sequence
的 $vau
以及 $let
等合并子不同,但和 eval
仍然相同。
求值得到的操作数的文法约定如下:
-
<object>
:一般对象,包括引用对象的引用值。 -
<reference>
:对象引用值。 -
<pair>
:有序对。- 注释 可构成真列表或非真列表。
-
<list>
:列表:空列表或第二个元素为空列表的有序对。 -
<lists>
:元素都是列表的列表。 -
<boolean>
:布尔值,值为#t
或#f
的集合。- 是类型映射指定的用于条件判断的单一值的类型。
- 推论:
<boolean>
对应的宿主类型是bool
。
-
<test>
:类似<object>
,通常预期为<boolean>
,作为条件。- 当求值结果非
#f
时条件成立。 -
原理 和 Scheme 类似但和 Kernel 不同,非
#t
的值在决定分支时视同#f
,以允许在<boolean>
外自然扩展的逻辑代数操作。 -
原理 和 Common Lisp 不同,不使用空列表(或符号值
nil
)代替#f
,以避免需要特设的规则以特定的其它类型的值(如 Common Lisp 的符号值t
)表示逻辑真(这在逻辑非操作中不可避免)。
- 当求值结果非
-
<combiner>
:合并子。 -
<applicative>
:应用子。 -
<operative>
:操作子。 -
<predicate>
:谓词,是应用操作数的求值结果的值为<test>
的<applicative>
。-
注释 通常实现结果是
<boolean>
的纯求值。
-
注释 通常实现结果是
-
<environment>
:一等环境。 -
<parent>
:指定环境的父环境的值,包括:- 环境引用值:
<environment>
或以<environment>
值作为被引用对象的<reference>
。 - 元素为环境引用值的
<list>
。 -
被引用对象是元素为环境引用值的
<list>
的<reference>
。
- 环境引用值:
-
<string>
:字符串。- 字符串是包括数据字面量作为表示的值的类型。
- 字符串的内部表示在具体实现中保持一致。除非另行指定,使用 ISO/IEC 10646 定义的 UCS 的 UTF-8 编码,其值不包含空字符(编码数值为 0 的 UCS 代码点)。
- 关于当前实现,另见 YSLib 项目文档
doc/NPL.txt
。 - 注释 为互操作的兼容性,一般建议实现使用兼容 [ISO C++] 中定义的 NTMBS(null-terminated multibyte string) 的方式表达。
- 此外,支持的数值操作数参见 NPLA 数值类型。
原理
<object>
等求值得到的操作数不保证是语法意义上连续的词法组合,不能由多个表达式构成,因此即便出现在元素末尾,也不能如 <body>
一样减少括号。
<object>
作为类型全集,其元素可被断言在论域内,即任何其它类型都是 <object>
的子类型。类型检查可对此进行验证。
和 [RnRK] 的理由不同,允许布尔代数以外扩展的比较判断在此不认为是易错的,而是有意的设计(by design) 。这避免预设地假定类型的名义语用作用(“角色(role) ” ),也避免限制语言和派生语言的类型全集的设计。
注释
空列表构成的单元类型是真列表的子类型,而不是有序对的子类型。
非空真列表是有序对的子类型。
- 除非另行指定,以
<symbols>
指定的值被作为<definiend>
或<formals>
使用时不引起错误。-
注释
<symbols>
在被其它上下文使用时仍可能引起错误。
-
注释
-
<symbols>
形式的符号列表在绑定变量名时支持引用标记字符&
和%
。符号作为被绑定的初值符时,移除符号中发现的这些引用标记字符。 -
<definiend>
和<formals>
不要求重复符号值检查。另见绑定操作。 - 使用
<formals>
的情形包括合并子基本操作和可通过这些操作派生的操作在对应位置的操作数。-
原理 [RnRK] 和 [Shu10] 都没有显式区分
<definiend>
和<formals>
,两者在上下文中实质可互换,差别仅在 [RnRS] 中的 define 形式中定义的位置可具有和<formals>
不兼容的扩展形式。 -
注释 这实质等价使用 [Shu09] 中的记法,即
<formals>
用于除和 [Shu09] 的$define!
类似外的所有操作(包括$set!
和$let
等,而不论是否对应<body>
)。这些上下文中总是隐含了上述的可派生实现的要求。
-
原理 [RnRK] 和 [Shu10] 都没有显式区分
-
<body>
不蕴含顺序求值子项。-
原理 这也允许
<body>
中的表达式被整体求值。
-
原理 这也允许
关于合并子基本操作,参见 YSLib 项目文档 doc/NPL.txt
。
原理
和传统 Lisp 方言(包括 [RnRS] 和 [RnRK] )的函数体不同,<body>
的各个表达式之间不蕴含顺序求值。
因此,和 [RnRK] 不同,$vau
不需要在基本操作之后再次派生。这使操作的功能更加正交。
注释
和 [RnRK] 不同,<symbols>
、<definiend>
和 <formals>
具有一些附加的约定支持;<body>
不蕴含顺序求值子项;NPLA1 的符号可通过代码字面量求值得到。
基于 NPLA 基本语法约定参见 NPLA 整体约定。
NPLA1 表达式符合 NPL 表达式语法。
注释
NPL-GA包含的转义规则中包含 <char-escape-seq>
的 <char-seq>
要求类似 [R6RS] 在字符串中的元素;其中仅有 \ 和 " 被 [R5RS] 直接支持,而 [R7RS] 不支持 \v 。
后者支持的其它转义字符序列词法可被派生实现以 <$literal-char>
的形式另行指定(其中 [R7RS] 可涵盖对应 <char-escape-seq>
的功能)。
派生实现可指定不同的字面量,但不应和已指定词法构造的记号冲突,包括本节指定的字面量。
NPLA1 字面量都是纯右值,但总是允许实质化转换为消亡值并引入允许互操作的临时对象。
注释 这和宿主语言的字符串字面量是左值不同。当前 NPLA1 对象语言不提供能引起互操作差异的接口(字符串字面量不被修改),以后可能改变。
字符串字面量的类型为 <string>
。
基于 NPLA 数值类型和数值字面量,NPLA1 数值字面量的类型为 <number>
。
除非另行指定,数值的具体宿主类型未指定。
注释 部分数值可指定具体的子类型。
NPLA1 支持 <integer>
类型的精确数数值字面量和 flonum 不精确数数值字面量。
支持的字面量包括词素符合 NPLAMath 数值表示的字面量。
派生实现可定义其它数值字面量。
注释
以上字面量包含十进制数值的字面量。其它字面量是 NPLA 扩展字面量。
无限大值和 NaN 值同 [Racket] 的字面量词法,除这些类型总是启用,且使用明确属于 flonum 且对应明确宿主类型的 long double 代替不被作为一般 flonum 的 extflonum 。
[SRFI-73](已撤消)提出扩展 [R5RS] 的带有 #e
或 #i
前缀的精确数和不精确数无限大值字面量,其中前缀及精确数的支持和 [RnRK] 类似。NPLA1 不支持无限大值精确数。
关于无限大值的在 [RnRS] 的一些实现情形,另见这里。
NPLA1 支持 NPLA 扩展字面量作为部分数值字面量。
NPLA1 还支持以下以 #
起始的扩展字面量:
-
#t
:布尔值逻辑真,类型为<boolean>
。 -
#f
:布尔值逻辑假,<boolean>
。 -
#true
:同#t
。 -
#false
:同#f
。 -
#inert
:类似 Kernel 的#inert
字面量, -
#ignore
:类似 Kernel 的#ignore
字面量。
原理
#inert
和 #ignore
类似 [RnRK] 。
从表达上,#inert
和 #ignore
仍都可以被视为特定单元类型的值:等价的类型判断谓词可以直接使用值的相等关系确定。
和 [RnRS] 及 klisp 不同,不需要因兼容性支持扩展字面量中不同的大小写变体,特别是 [R6RS] 的 #T
和 #F
。
和 [RnRS] 类似而和 [RnRK] 不同,NPLA1 表达结果结果通常不依赖 #inert
,而直接使用未指定值。这避免用户必须引入 #inert
等具体的值实现相同隐式效果而违反关注点分离原则。
尽管在接口意义上通常是不必要的,若有需要(如派生结果等效 #inert
的操作),#inert
的值仍可被显式使用。
注释
[R5RS] 和 [RnRK] 指定 #t
和 #f
。[R7RS] 指定同义的 #true
和 #false
(参见 R7RS ticket 219 )。
后者被认为可提供若干可读性,但具有冗余。本文档中,以下不使用 #true
和 #false
替代 #t
和 #f
。
派生实现可扩展支持,提供非 <boolean>
类型的布尔值,使用与这些字面量不同的对应表示。
以下使用 ...
作为函数的操作数时,可支持没有操作数的函数合并。此情形下应用表达式仍需要前缀 ()
,但不在以下规约文法中显式地表示。
注释
和 Scheme 及 Kernel 不同,求值算法决定求值为函数合并表达式的语法表达不需要括号,且具有不同的函数合并形式。
对象语言可能提供关于内存安全的检查。
除非另行指定,假定实现进行互操作无法保证内存安全。
对象语言提供关于内存安全的基本保证:不存在违反内存安全相关的要求以外的未定义行为(包括循环引用等)、不存在不保证内存安全的互操作且不存在不安全间接值访问时,对象语言的程序执行保证内存安全。
非内存安全操作在对象语言中以不安全间接值访问的一部分情形体现。
不安全(unsafe) 操作是可能在程序的执行中引入未定义行为的操作。
这里的未定义行为包含在操作中直接引入的未定义行为,以及因为操作被执行而使程序在之后无法确保排除的未定义行为。
不安全操作是实现可选提供的。
当前对象语言不支持并发访问对象。数据竞争仅可由和宿主语言的互操作引入。
对象语言的不安全间接值访问包括:
环境对象被销毁导致作为间接值的环境引用被无效化。
注释
另见环境生存期。
作为间接值的引用值是间接值的实例,因此无效的间接值包含无效的引用值,通过无效间接值访问包括无效的引用值的访问。
对象语言不提供悬空引用以外构造无效引用值的操作。
对象语言中可引入悬空引用的情形包括:
关于间接保留引用值和互操作可能引入悬空引用的情形,参见 YSLib 项目文档 doc/NPL.txt
。
注释
另见对象语言的引用值。
使用其它不保证内存安全的操作可引入不具有内存安全保证的间接值访问实体。
对象的(直接或间接)子对象是间接值时,对象包含间接值。
修改对象为间接值或使之包含间接值时,对象保留间接值。
被保留的间接值是对应的通过修改得到或包含的间接值。
本节中的概念对应适用于具体的间接值,如被保留的引用值和函数在结果中保留引用值。
函数调用返回(在对象语言中允许出现的,下同)间接值或包含间接值的对象时,在函数值中保留间接值。
函数调用修改环境使环境对象保留间接值(绑定间接值或包含间接值作为子对象的对象作为被绑定对象)时,在环境中保留间接值。
函数调用修改一等对象或其子对象,使之保留间接值时,在对象中保留间接值。
在函数值中保留间接值、在环境中保留间接值、在对象中保留间接值的函数保留间接值。
被保留的间接值被函数调用的求值结果蕴含时,函数在结果中保留间接值。
注释
在结果中保留间接值包含以下情形:
- 在函数值中保留间接值。
- 在环境中保留间接值,环境是函数值或其子对象。
- 在对象中保留间接值,对象是函数值的子对象。
函数返回包含间接值的对象由参数的值决定时,保留参数中的间接值。
按被保留的间接值的来源,这分为以下两个子类:
操作可保留间接值:
- 使用函数调用实现的操作可通过函数调用保留间接值。
- 其它实现方式可等效地保留间接值。
注释
在结果中保留间接值的操作不区分被保留的间接值的来源和目标。但多数情形下,这通过函数值保留参数中的间接值蕴含。
保留间接值操作的内存安全的一个必要条件是所有被保留的间接值在之后的使用中都满足内存安全。
保留间接值在操作后可能因间接值无效(如悬空引用),无法继续保证内存安全。
在环境中保留间接值时,应保证环境具有足够的生存期,以避免间接值依赖无效的环境引用导致访问环境中对象的未定义行为。
对象语言接口的安全保证机制提供不同接口的分类,通过允许区分是否具有内存安全保证的接口帮助程序利用对象语言基本内存安全保证。
通过避免或限制使用不安全操作,实现上述安全保证。
因为允许引入 NPLA 未定义行为,无法提供安全证明的互操作应视为不安全操作。
基于求值算法的安全保证的非形式的证明框架概述如下:
- 任意步骤中,访问间接值指定的目标对象是安全的,仅当间接值是安全的。
- 符号值的求值是安全的,仅当引用的环境是安全的。
- 合并子调用的求值是安全的,仅当合并子、操作数及调用的操作是安全的。
原理
满足安全保证的推理如下:
- 因为 NPLA 实现的非互操作引入的、非求值规约的管理规约不存在未定义行为,以上求值算法中的步骤中通过排除不安全的实体能保证规约中不存在未定义行为。
- 因为规约决定程序执行的语义,在求值中排除不安全的实体可以保证不存在未定义行为,而满足安全保证。
一些不安全操作是否蕴含未定义行为可能依赖具体调用使用的操作数。
若能证明特定的前提保证任意的调用实例中的操作数满足附加的安全假设,则这些不安全操作的调用仍可保证安全。
排除不确保安全性假设的互操作时,NPLA1 提供附加调用安全:若不存在隐藏环境中绑定的可修改对象的引用,则仅因可能违反值稳定性的不安全操作的调用是安全的。
派生实现可对特定调用附加使用限制以便提供证明,或定义其它的调用并提供更强的保证。
引起诊断时求值被终止,或在失败可被恢复时以其它派生实现定义的方式继续求值。
其它引起诊断的条件可被派生实现补充指定。
注释 注意未定义行为取消对诊断的要求。
本节以外的诊断消息的其它形式未指定。
注释
引起诊断的求值包括:
其它求值条件详见具体操作的规定。
NPLA1 中的错误是按接口的约定不符合预期的正常条件(如不被正常处理的操作数类型)引起的诊断。
求值特定的表达式可引起错误,包括:
- 违反求值算法步骤中的要求而直接引起的语法错误(syntax error) 。
- 其它情形引起的语义错误(semantic error) 。
以接口文法约定的形式约定的操作中,除类型检查外,绑定初始化之前的参数绑定失败是语法错误。
语法错误包含两类:
- 总是依赖程序运行时确定的值不满足特定操作的要求引起动态语法错误。
- 其它语法错误违反语法正确性要求,是静态语法错误。
类似地,语义错误包含两类:
- 总是依赖程序运行时确定的值不满足特定操作的要求引起动态语义错误。
- 其它语义错误违反语义正确性要求,是静态语义错误。
静态语法错误可能通过语法分析从源代码决定。
引起动态语法错误或动态语义错误依赖的值是合并子的具体实际参数的值,以及派生实现可选指定的其它的值。
引起动态语法错误或动态语义错误的情形包括求值特定的函数应用,由具体操作指定。
程序可通过引发(raise) 一个错误对象(error object) 指定引起诊断。
除非另行指定,NPLA1 的错误对象不需要是 NPLA1 支持的对象,而可以仅在宿主环境中可见。
因果性引起的错误可构成错误之间具有依赖关系。
错误对象的其它具体形式由派生实现指定。
NPLA1 的当前诊断使用的异常执行机制由宿主语言支持,通过宿主语言中的异常类型区分不同的异常条件。
NPLA1 约定的所有要求引起异常的诊断情形都是错误。
注释 用户操作引起异常不一定是错误。
不引起未定义行为的翻译失败应抛出异常。
引发错误对象可能通过抛出异常实现。此时,被抛出的宿主语言异常对象是错误对象。被抛出的异常类型可具有被显式指定的 public
基类,这些基类应无歧义以允许宿主语言捕获。
若存在依赖错误且引发被依赖的错误对象使用抛出异常实现,使用宿主语言标准库的嵌套异常(nested error) 机制实现依赖错误。
当前没有提供相关操作,但抛出的宿主异常在具有表示的意义上是 NPLA1 的一等对象。
关于抛出异常的宿主类型,参见项目文档 doc/NPL.txt
。
除非另行指定,实现应对以下全局的运行时错误条件按要求引起诊断。
当实现无法提供需要的资源,资源耗尽(resource exhaustion) 。此时,引发特定的关于资源耗尽的错误对象。
除非另行指定,上述表示资源耗尽的错误对象满足宿主语言的以下类型的异常对象:
- 宿主资源耗尽时,异常类型满足常规宿主资源分配要求的类型。
- 否则,同异常。
注释 [ISO C++] 的本机实现宿主资源耗尽时,一般抛出派生 std::bad_alloc
的异常对象。这不包括本机实现无法提供资源的未定义行为。
检查(check) 是限定成功的操作应满足的(必要非充分)条件引起诊断的操作。检查失败时要求引起诊断。
良定义的检查应具有强规范化性质,以保证有限数量的检查总在有限的计算步骤内终止。在进行检查的上下文,实现假定检查良定义。
注释 实现不需在此之前对检查的这个性质附加检查。
检查条件限定检查的通过或失败。除非另行指定,通过的检查没有作用,失败时总是具有作用。
注释 检查失败通常可引起副作用。
NPLA1 要求在特定上下文进行类型检查。派生实现可定义其它检查。
函数操作的语义可单独指定检查,具体形式由具体操作指定。
基于名义类型,对象语言实现应具有语义规则指定的类型检查,以确保程序的执行符合操作的必要前置条件。
操作的语义可要求以下的类型检查:
实现可能添加其它不违反语义要求的类型检查。
基于表达式的类型,对应对象语言表达式的表示实体的元素可指定操作数上明确的类型要求。
部分实体从属于其它实体类型而构成子类型关系;部分的规约操作取得求值结果保证结果中的值可能具有的特定类型集合,这些类型也一并在以下描述中给出;其它情形不指定类型。
规约预期符合约束。若违反由项的表示的对象的动态类型不匹配导致,则求值失败;否则,行为未指定。
类型检查的完成应先序依赖被检查特定类型的值的访问。
除非另行指定,类型检查和程序中的其它作用(包括不同的其它类型检查)的顺序未指定。
类型错误引发错误对象。
若合并子调用不接受非真列表参数构成函数合并,检查参数是真列表,即参数列表(parameter list) 。对参数列表的类型检查的完成应先序于其中任意子表达式的求值。
原理
类型检查有助于维护程序的正确性,并及早发现编程错误。
但是,类型检查自身存在开销;在一个阶段中集中检查类型的限制不是必要的。特别地,静态类型检查不被要求。
这些设计同时确保程序容易在程序在实现的不同执行阶段重现相同的检查逻辑乃至直接复用其实现。
为减小开销等目的,实现可能合并不同类型检查,而不改变程序的可观察行为。
对子表达式的求值需访问子表达式。因此,对参数列表的检查蕴含顺序要求。
注释
一个值可被多次针对不同的对象进行类型检查。
不同的类型检查中,对特定类型的值的访问之间没有必然的隐含联系。
外部表示若被确定,由实现和派生实现定义。
NPLA1 不要求对象和其它实体存在外部表示,也不要求外部表示唯一。
注释
对外部表示的存在性要求和 [RnRK] 不同。
NPLA1 当前直接使用其它已被指定的表示规则,如互操作隐含的宿主语言对象表示。
NPLA1 当前不提供可移植的互操作接口(包括一些基本 I/O 操作),也不约定其涉及的外部表示形式。
部分语义不需要通过求值体现。
以被求值的表达式和所在的环境作为参数,NPLA1 使用以下规范(canonical) 求值算法取得表达式的求值结果:
- 自求值:若被求值的表达式不是符号值且不是有序对,则求值结果是自身。
- 名称解析:若被求值的表达式是一个符号值,则被视为变量名,求值结果是它在上下文(当前环境确定的词法作用域)中变量绑定确定的对象的经引用折叠的左值引用。
- 否则: 注释 被求值的表达式是有序对。
有序对以外的表达式被求值时:
- 标识符的值是构成标识符的符号值。
-
代码字面量的值是去除其边界的
'
的标识符构成的符号值。 注释 代码字面量可表达直接作为标识符时不能作为符号值的词素的转义,例如''
是一个空的符号值;而'#ignore'
和42
这样的形式允许其中的表达作为变量名,而不是字面量。 -
#t
和#f
求值为自身,是布尔值。 -
#ignore
和#inert
求值为自身,具有和其它值不同的单元类型。 - 数值字面量求值为数值(numerical value) 。
非空列表和代码字面量以外的对象作为表达式,都是自求值表达式。
原理
NPLA1 规范求值算法和 [RnRK] 中定义的 Kernel 求值算法(以及 [Shu10] 中定义的 vau 演算)类似,差异为:
最后一个差异在对象语言中是实质性的,它决定列表表达式和其中的子表达式的求值总是等价。
求值算法保持当前环境。
NPLA1 翻译单元中,未求值的表达式满足以下性质:
NPLA1 规范求值算法和 [RnRK] 的求值算法具有近似的简单性。
因为 NPLA1 不支持存在环的非真列表,cons 对的描述被对应替换。
求值算法使用的环境同 [RnRK] 。
同 [RnRK] ,而非 [RnRS] ,NPLA1 规范求值算法避免对顶层(top-level) 的特殊引用,以避免上下文相关性的不同规则带来的复杂性和限制。
使用顶层的不同求值规则的限制可能简化一些编译实现需要的假设。但这泄漏了抽象,且在实际使用中引起大量问题。
特别地,不同的顶层的特设规则相对更动态,反映一些用户对 fexpr 的期望,但在此这已被 vau 抽象替代。因此,使用不同的顶层求值规则以提供更强的动态性是多余的。
另一方面,当前环境一般允许被具现为一等环境在程序中可编程地访问而代表求值算法使用的上下文。为不同的上下文特设不同的顶层求值规则也是多余的。
关于实现,参见 YSLib 项目文档 doc/NPL.txt
。
以下各节补充描述 NPLA1 规范求值算法的局部性质。
注释
关于 WHNF 求值在 Kernel 中的描述,参见 [RnRK] 关于 unwrap
的 Rationale 的描述。
语法分析器的实现应使结果取得和这些性质兼容的中间表示。
求值算法向函数合并传递当前环境作为函数合并的动态环境。
为支持没有操作数的函数应用,需约定其它表达式表达求值为函数合并的应用表达式:
- 当复合表达式的第一个子表达式是空列表(
()
)时,求值为一个函数合并。- 注释 对没有操作数的情形,这是唯一被直接支持函数应用的语法
- 否则,求值的作用同移除第一个子项
()
后的剩余形式。
注释
关于区分函数类型的替代设计(使用 $
作为第一个子项)的一个例子,参见这里。
基于其中类似的对语义的影响(区分函数合并是否针对一个操作子)上的理由,这不被使用。
与此不同,尽管在对象语言中接受 ()
的使用也需要求值算法的显式支持,这在目的上是纯语法意义上的——仅在无法避免语法歧义时,才必须使用。
只要能确定求值算法使用的环境,就能静态地区分复合表达式是否是函数合并。此时,其中的第一个子表达式是否显式为 ()
不影响关于语义的推理。使用不同的内部中间表示可完全消除是否使用 ()
的函数合并的差异;或者,也可以约定在代码中默认使用第一个子项是 ()
的表达式作为函数合并的规范形式,而把第一个子项不是 ()
的形式视为隐含 ()
的语法糖。
因此,相对使用 $
而言,使用 ()
的设计具有更少的缺陷(尽管需要更多的字符)。
空列表 ()
作为表达式是自求值表达式,而不是没有函数的空过程调用。
原理
关于 ()
的求值规则避免这种简洁有用的语法导致语法错误。
注释
这和 [RnRS] 不同而同 [RnRK] 。在前者构造空列表需要 '()
。
和 Kernel 不同的函数合并求值规则使这个设计和函数求值字面上没有直接的关联,避免了 Kernel 中为什么 ()
不是词法上类似的如 (f x)
这样的表达式的特例的问题。
注意以 ()
作为前缀并不要求要求特定函数的子类型而可能破坏子类型封装性的假设。
具有不同大小写字符的标识符不同。
可使用(能在求值时作为名称的)代码字面量即 '' 分隔)表达没有分隔符时被解释为字面量或其它值的符号值。
符号值作为名称表达式,经名称解析求值,访问当前环境中的被绑定对象。
其中,若被绑定对象是引用值,结果是被折叠一次的引用值;否则,结果是被绑定对象作为被引用对象的引用值。
求值的结果是确保为左值引用值。
结果不继续特别处理。引用值在此作为一等对象,作为表达式时不发生左值到右值转换。
注释
标识符大小写敏感的设计和 [R5RS] 及 klisp 不同,而和 [R6RS] 相同。和 [R7RS] 的默认行为相同,但不提供切换大小写不敏感的方法。
代码字面量和 klisp 使用 ||
作为分隔符的语法不同,但作用类似。
和 klisp 不同,NPLA1 允许使用 .
作为变量名,但在特定的上下文不被求值时符号值 .
可被特别处理,如绑定匹配时忽略以 .
为符号值的绑定。
和 klisp 不同,NPLA1 允许使用 ++
等全以 +
或 -
组成的字符序列构成标识符。
以 #
、+
或 -
起始的不能构成标识符的词素是 NPLA 扩展字面量。
除非另行指定,NPLA1 对象语言的求值总是使用 NPLA1 规范求值算法。
在输入求值算法接受的语法形式之前,求值使用基于中缀语法识别的分隔符进行处理。
由此引起的其它语法差异参见绑定构造。
NPLA1 提供符合特定谓词指定的过滤条件的中缀分隔项替换为特定名称表达式指定的前缀操作形式的列表。
这些中缀变换作为预处理操作,可识别和接受 NPL-GA 语法外的记号,即转换扩展的 NPL-GA 文法输入为严格的 NPL-GA 语法要求的源语言。
中缀变换递归替换构成表达式的形如 <expression> (<infix> <expression>)*
的记号序列为 <transformed-infix> <expression>+
形式的记号序列。
其中,被支持的中缀记号 <infix>
是 ;
或 ,
,而 <transformed-infix>
是语法不可见的中缀变换函数。
其中,分隔符 ,
优先组合。
分隔符对应的 <transformed-infix>
分别表示对被分隔的序列参数进行有序和无序列表求值(替换后合并子功能对应参照实现环境中函数 $sequence
和 list%
求值后的合并子)。
对分隔符的处理使用和组合顺序相反的两遍分别对 ;
和 ,
遍历替换。
变换的不同 <expression>
的实例以相同的词法顺序在变换后的结果中被保存。
原理
和 Scheme 不同而和 Kernel 类似,求值通常使用显式的风格(详见 [Shu10] )而不是依赖 quote
的隐式风格;这和不需要括号的语法特性无关。
特定的表达式维护可修改性。
注释 这类似宿主语言的 const
类型限定,但只适合左值且仅使用隐式类型。
特定的操作集合可约定关于确定结果值类别和类型的具体规则,如子对象访问约定。
绑定操作决定符号值或具有符号值的数据结构与项的对应关系,并初始化被绑定对象而引入变量。
作为函数语法的推广,两者分别由绑定操作使用形式参数和操作数指定。
为决定形式参数对应的操作数,形式参数和操作数树或它们的子对象的结构被比较,即绑定匹配(match) 。匹配操作数树的形式参数对应也可具有树的构造,即形式参数树(formal parameter tree) 。
被匹配的操作数是操作数树作为有序对的元素。类似地,形式参数是形式参数树作为有序对的元素。
绑定操作初始化对应的变量的名称和值分别由形式参数树和操作数树决定。
NPLA1 形式参数树具有特定的语法规则:树的叶节点为符号值、符号的引用值或其它形式参数树构成的 DAG 。若构造的形式参数树不符合语法规则,引起错误,不进行绑定。
成功的匹配决定形式参数对应的操作数或其子项,作为其实际参数。这种对应关系是单射但不一定是满射,即匹配成功后,每个参数总存在对应的操作数或其子项,而操作数和子项允许不对应形式参数而被忽略。
被绑定的项的操作数中的元素对应是项中的元素。
形式参数树中的引用值可能被间接访问其被引用对象一次,其余元素在匹配时被视为右值。
绑定操作符合以下节的绑定规则。
原理
被绑定的参数可作为函数的形式参数。绑定操作对形式参数的处理也可以作为其它初始化变量的语法构造的基础。
作为推广,绑定操作也可以引入函数的形式参数以外的变量。
注释
形式参数树的节点可以是符号的引用值,但不支持多重引用。
关于对形式参数树的具体的语法要求,另见 <ptree>
的定义。
因为 NPLA1 支持的绑定构造都具有函数合并的形式,操作数或其子项总能直接被作为函数的实际参数。
DAG 要求和 Kernel 类似。
和 Kernel 不同,操作数树同时支持作为引用的左值和非引用的右值,在实现上需要解析引用。
绑定的对象节点的值和子节点元素被复制初始化。
绑定前不对形式参数或实际参数中的元素求值。
除非另行指定,不同变量的绑定初始化之间非决定性有序。
绑定初始化不修改形式参数,但可能因初始化转移初值符而修改操作数。
注释
初始化元素类似宿主语言的参数传递中可发生初始化。
若形式参数或实际参数可能由求值得到,需在匹配前另行处理。
由非决定性规约规则,一般地,变量仅通过初值的求值决定的依赖关系及子对象决定之间初始化的相对顺序。
因为绑定的初始化不负责实际参数的求值,一般地,即使初值符位于相邻的语法构造,也不保证隐含顺序;这和宿主语言不同。
初始化的顺序规则和宿主语言初始化不同的函数参数类似。
原理
因为记号求值保证求值符号值是左值,被绑定的对象名称解析最终得到的引用值不包含唯一引用属性。
这不清除绑定临时对象引入到表示被绑定对象的项或引用值中的其它属性,因此其它属性可跟随一等对象被跨过程传递(若不经过返回值转换或其它操作)。
同绑定临时对象属性的讨论,被传递的属性类似宿主语言的指定转发引用参数类型,以下记作 P
。
特别地,被传递的属性包含临时对象属性。这对应宿主语言中 P
是左值引用。
跨过程传递并不被宿主语言支持。因此,一般仅限为了实现类似宿主语言的根据值类别和类型转发参数的转发上下文(forwarding context) 中使用。
通过从传递的属性中提取的标签访问引用引用值的属性代替保存环境引用并以其它底层的方式查询作为被引用对象的被绑定对象的元数据能以更低的开销实现一些常见的相同的目的,如判断被引用对象是否表示可被转移的资源。
注释
使用引用标记字符可保留来自引用值实际参数的作为引用值属性的临时对象属性。
使用引用标记字符 &
可启用转发推断值类别。
绑定匹配以一个形式参数树和操作数树作为输入,比较两者的结构并尝试关联形式参数树中的子项到操作数蕴含的对象,以创建变量绑定。
若绑定匹配成功,则可能进行以符号值为名称的对应变量的绑定初始化;否则,绑定匹配失败,引起错误。
绑定匹配确定每一个符号值的过程先序这个符号值确定的变量的绑定初始化。
绑定匹配不修改形式参数,在匹配成功进行绑定初始化前不修改操作数。
匹配使用如下算法搜索形式参数树和操作数的对应位置:
- 初始化输入的形式参数树为当前形式参数,函数合并构成的操作数树作为当前操作数。
- 对每一对当前形式参数和当前操作数,比较两者(除非另行指定,操作数的值是引用值的,视为匹配被引用对象,下同):
- 若两者都是有序对,则:
- 若形式参数有序对元素的结尾元素不是符号也不是有序对,则参数匹配失败。
- 若形式参数是列表,且元素的结尾元素是以
.
起始的符号值,则存在省略(ellipsis) ;保存移除.
的符号值,并从子项中移除结尾元素,继续进行比较。 - 若形式参数和操作数的(直接)前缀元素数相等,或存在省略时移除结尾元素后的形式参数前缀元素数不大于操作数子节点的元素数,则:
- 注释 直接比较前缀元素数,不计算有序对的非前缀元素是引用值且其被引用对象是非空列表时具有的元素数。
- 忽略形式参数中的省略的元素,以深度优先搜索从左到右逐一递归匹配两者的元素。
- 若存在省略的元素,若保存移除
.
的符号值非空,以移除.
的符号值作为形式参数,匹配操作数构成的结尾序列(trailing sequence) 。 - 否则,若形式参数是非列表的有序对(最后的元素非空),匹配结尾序列。
- 注释 结尾序列支持匹配有序对操作数的非前缀元素。这个元素可能是引用值,它的被引用对象被作为操作数继续匹配并进行非递归绑定。
- 否则,若所在的形式参数列表的结尾元素是
.
,参数匹配成功,忽略结尾序列,不绑定对象。 - 否则,没有其余元素需要匹配,参数匹配成功。
- 注释 先前对形式参数和操作数的节点数判断同时确保结尾序列为空。
- 匹配结尾序列的规则参见非递归绑定。
- 注释 结尾序列预期匹配的操作数是空列表或有序对。对操作数是列表的情形,结尾序列是结尾列表(trailing list) 。
- 否则,若不存在省略,列表的元素数不相等,参数匹配失败。
- 否则,操作数的子节点不足,参数匹配失败。
- 若形式参数是空列表,则:
- 若实际参数不是空列表,则参数匹配失败。
- 否则,参数匹配成功。
- 若形式参数是引用值且没有因为本条匹配规则递归进入匹配,则以其被绑定对象代替当前形式参数递归匹配。
- 若形式参数不是符号,则参数匹配失败。
- 若形式参数不是
#ignore
,则尝试绑定操作数到以符号值确定的名称的形式参数。
- 若两者都是有序对,则:
绑定匹配时不检查重复的符号值。若形式参数树中出现重复的符号值,可被多次匹配成功。这可导致之后的绑定初始化中,只有其中某个未指定的绑定生效,其它绑定被覆盖。
原理
虽然可能匹配被引用对象,操作数匹配不蕴含时引用值不被消除。
和 [RnRK] 不同,明确直接比较前缀元素数,因为:
- 这允许在元素数不同时给出更具有针对性的诊断,避免误用。
- 这能避免匹配在任何情形都总是顺序地依赖每一个操作数的值,允许并发实现。
实现使用的表示允许访问元素数具有 O(1) 的时间复杂度,而访问前缀元素数具有 O(n) 时间复杂度。但限制不访引用值时,不会有较大的附加开销。
注释
函数合并构成的操作数树包括作为合并子的第一个子项和作为操作数的之后余下的子项。
数据结构和匹配算法类似 Kernel 中用于 $define!
和 $vau
等操作子的递归的匹配机制,但有以下不同(另见 NPLA1 合并子):
- 不支持 cons 对的中缀
.
,但支持形式参数树中的列表最后以带省略的符号值匹配多个列表项的参数,绑定结尾序列。 - 对参数子项的符号值中可选的
.
起始以及之后可选的前缀作为标记字符作为引用标记进行处理。 - 不提供转义,若符号值去除可选的前缀及标记字符
.
后为空则忽略绑定。 - 若参数子项按引用传递则间接访问并绑定被引用对象。
- 只支持无环列表,且不检查(因为 API 已经保证只支持真列表)。
- 列表外的
.
起始的词素当前视为普通的符号,但此行为可能会在未来改变)。
被忽略的绑定不保存绑定的对象。
不在列表内最后位置的带有前缀 .
的形式参数绑定的是普通的变量,不忽略绑定。
和 Kernel 不同,不检查重复符号值,且绑定匹配对特定模式的形式参数进行不同的处理。
其它一些不支持 cons 对的语言,如 [ECMAScript 2019] 的 rest
参数支持类似结尾列表的效果。
绑定匹配和创建绑定的初始化之间的顺序约定是必要的,因为这里约定的是一般的规约规则而非求值规则,递归蕴含规则等求值的默认规则不适用。
绑定匹配允许并行化。
应用在形式参数树叶节点符号值的前缀 %
、&
或 @
为标记字符表示名称绑定的可按需引入引用,称为引用标记字符(sigil) 。
绑定引用时,可使用引用推断规则:
标记字符引起的绑定的差异为:
- 不存在标记字符时,对操作数按值的副本绑定,实际参数的值的副本传递给对应的形式参数。
- 存在标记字符
%
或&
时,按上述的引用推断规则直接绑定或转发操作数。- 当实际参数是引用值时,在可能对其它属性进行的处理后,隐含一次引用折叠。
- 存在标记字符
@
时,绑定以实际参数作为被引用对象的引用值,不论操作数的类型和值类别。- 初始化引用值时,没有引用值的消除。
注释
除复制消除转移有序对操作数的子对象外,绑定时不修改被绑定操作数。
支持修改操作数的绑定的其它标记字符可能在未来支持。
非递归绑定在一次匹配之后创建对应的变量绑定。
合并使用或不使用引用标记字符的情形,非结尾序列的单一参数对象的绑定初始化包含以下过程:
- 若不存在标记字符
@
,则: - 否则,被绑定对象的是操作数的引用值:
- 绑定操作数的引用时,要求引用的是列表中的项,否则引起错误。
- 被绑定的对象应是不唯一的值(直接绑定操作数右值以外的值),被绑定对象是操作数的引用值。
绑定结尾序列包含以下情形:
- 若不存在标记字符
@
,则:- 若操作数为可转移的对象的引用值,按非结尾序列的规则绑定操作数。
- 否则,若操作数属性指定可修改的临时值或有标记字符
%
时的临时值,按非结尾序列的规则绑定操作数。 - 否则,创建新的有序对,在其中以相应的标记字符(若存在)绑定各个元素子对象。
- 否则,创建新的有序对,在其中以标记字符
@
绑定各个元素子对象。
绑定结尾序列创建新的有序对并绑定元素子对象时,作为列表完全分解得到的每个元素组合的列表,满足:
- 若操作数是临时对象,则操作数子项在绑定元素子对象时被复制消除。
- 组合的列表是非真列表,当且仅当操作数是非真列表。
- 子对象的元素是对应的操作数以对应的引用标记字符(若存在)绑定单一参数得到的值。
- 注释 若不存在引用标记字符,元素被对应复制初始化。
- 若操作数是非真列表:
- 注释 此时需初始化组合中的非列表结尾元素。
- 当不存在标记字符或存在标记字符
%
时,组合的最后一个元素是操作数中的最后一个元素的副本。 - 否则,组合中的最后一个元素是新创建的子对象引用。
- 其被引用对象的表示中没有子项。
- 创建的有序对初始化完成后,参与初始化被绑定对象:
- 被绑定对象的元素总是不具有临时对象属性。
- 原理 这使实现能避免临时对象的子对象具有临时对象属性。
绑定临时对象外的引用临时对象视为对被引用对象的访问。
注释 这意味着除绑定临时对象外,若绑定操作数的初始化的引用值时实际引用临时对象,则因超出生存期的对象访问,行为未定义。
仅在绑定临时对象且操作数可转移或使用标记字符 %
时使用复制消除。
原理
绑定的默认行为对引用值特殊处理,是为了满足 G1b ,而不是像某些语言(如 [ISO C] 和 [Rust] )仅通过内建的机制提供特定的左值上下文(lvalue context) 。
绑定的默认行为不使用析构性转移的操作(类似 [Rust] 的设计),原因是考虑到绑定的副作用影响操作数(即便因为对象被销毁而不一定是修改操作)和破坏幂等性(特别是指定过程调用的形式参数时)违反易预测性原则。
为允许调用宿主对象的转移构造函数,限制复制消除。初始化引用之外的参数创建也不是 [ISO C++17] 约定要求消除复制的上下文。
作为操作数的引用值中的唯一引用在使用 &
引用标记字符时可同时蕴含绑定临时对象属性,这使绑定为变量的消亡值可能以名称表达式求值结果(不会是消亡值)的引用值访问时,能和其它引用值区分。提供这种设计的理由是:
- 以下两种涉及消亡值的资源访问可被统一:
- 直接访问消亡值表达式。
- 消亡值表示即将被转移的资源。
- 以消亡值初始化一个带有
&
引用标记字符的非结尾序列变量,并以这个变量的名称作为表达式进行访问。- 这通常需要使变量指称消亡值引用的资源,而不仅仅是表示即将被转移的消亡值自身。
- 具有临时对象属性的引用值通过右值初始化,相当于宿主语言中的右值引用,典型地表示能被转移的资源(而不一定需要立刻被转移)。
- 初始化变量同时转移资源,相当于宿主语言中复制初始化时调用转移构造函数转移操作数的资源到变量(对象或绑定到临时对象的引用),使之表示转移后的资源。
- 尽管值类别可能不同,这两种表达式都可以表示蕴含被转移的资源的对象。
- 直接访问消亡值表达式。
- 直接求值名称表达式往往比其它替代方式更直接高效,但结果总是左值而不具有唯一引用属性,而使用临时对象属性允许在求值的结果中被保留。
- 基于上述规则,对象语言中特定的转发操作处理可统一的方式处理两种表达式以转移资源。这种设计能简化一般的使用。
- 引用值支持临时对象属性对有效的转发对象应用子的实现是必要的。
- 通过对象属性,转发对象操作可避免总是从实际的操作数提取值类别的需要,允许作为应用子而非操作子。
- 若不使用临时对象属性,则需要其它方式编码和值类别不同的状态以和消亡值区分,例如宿主语言的静态类型信息。
- 这会增加语言规则的复杂性。
- 需要转发资源时,一般只需要使用转发对象操作;其它情形可安全忽略引用值中的临时对象属性。
- 类似地,在宿主语言中,编码在类型系统中的状态在特定上下文中用于实现完美转发。
- 宿主语言中,右值引用类型的变量作为左值(而不是消亡值)被访问,在大多数操作中没有和其它左值区分的意义。
- 在
std::forward
这样需要区分引用类型的转发操作(实例是一个函数,而不是宏)中,右值引用类型在局部是有意义的。 - 但是这仍然存在限制:因为没有跨过程传递的状态支持,明确具体类型还是需要程序显式指定
std::forward
的类型参数(或者宏),而不是 C++ 函数(应用子)的方式实现。
- 引用值支持临时对象属性对有效的转发对象应用子的实现是必要的。
- 消亡值应和纯右值在初始化其它变量时转移资源的作用一致,对应唯一引用属性和临时对象属性的相似处理。
- 通过唯一引用属性仍可区分一个具有临时对象属性的引用值以消亡值还是纯右值初始化。
- 可使用
&
以外的引用标记字符避免这里的行为而被初始化的被绑定对象(引用值)中引入非预期的临时对象属性。
绑定结尾序列和非结尾序列的非递归绑定规则略有不同。
- 特别地,除非被绑定对象是引用值,引用标记字符(不论是否存在)同时被作用到作为一等对象的元素上。这是因为:
- 此时,需要把操作数作为一等对象进行分解,使用引用标记字符或者不使用引用标记字符不破坏其它语义规则。
- 和非引用结尾序列相比,这使有序对的两个元素在初始化时的规则不同,但这具有合理性,因为:
- 有序对作为(非真)列表时,结尾元素和其它元素的地位不是相同的。
- 有序对的元素在 NPLA 对象表示中即已不对称,地位不可交换。
- 这种设计简化了一些重要的派生实现。
- 蕴含绑定临时对象属性的规则不适用绑定结尾序列中的元素,因为:
- 结尾序列的元素不具有名称,而不是通过绑定创建的能作为名称访问表达式的变量。访问元素需通过其它方式(如对象语言中的子对象访问操作),方法和结果不唯一(如可能具有不同的值类别,可能即时转移资源等),不具有和消亡值的统一性。
- 引入附加的临时对象属性容易引起非预期的转移。除子对象访问时可能发生的直接转移(通常较明确),随引用值跨过程传递的临时对象属性在之后可能继续引起其它转移。因为首先通过子对象而非名称表达式访问,这种转移的存在性在创建变量绑定的位置通常不显然而无法预知,容易误用。
- 一旦不需要附加的临时对象属性,去除属性而得到引用值通常是较为困难的,需要对象语言提供特设的操作或绑定新的对象(尽管引入临时对象属性可能同样困难)。
注释
引用折叠的结果满足不可修改引用属性的传播性质。其它情形应满足 NPLA1 引用值使用约定。因此,仅有使用标记字符 %
进行消除引用时,被消除的引用值的不可修改属性被忽略。
绑定临时对象外不和 [ISO C++] 一样可能延长右值类类型子对象的生存期。
具有引用标记字符的形式参数支持引入引用值并支持绑定引入临时对象的实际参数。
形式参数树子项和操作数树的子项成功匹配后绑定子项。
递归的绑定匹配对应递归的绑定创建,允许以操作数树的子项对应初始化形式参数树的各个子项。
注释 和形式参数树中的结尾列表的符号值被视为整体不同,递归绑定可包含项和其子项的多次递归的匹配。
绑定算法应确定和当前处理的操作数树的属性,即操作数属性(operand property) 。其中蕴含的表示操作数的项对应的标签,称为操作数标签(operand tags) 。
操作数属性和形式参数的引用标记字符结合决定是否按引用传递初始化,并判断绑定初始化时是否允许转移。
绑定匹配递归处理子项时,应确定子项的操作数标签,以指定子项可能具有的上下文相关的差异。
绑定初始时,操作数应为纯右值。此时,以临时对象标签作为初始操作数标签。
注释 这指定指定操作数是可被唯一使用的临时值。与此不同,若项表示作为一等对象的求值结果,应不具有临时对象标签。
一个项的子项的操作数标签由这个项的操作数标签(处理子项时,代表先前确定的所在的项的标签;以下称为当前操作数标签)和本节中以下约定的子项继承规则决定:
绑定临时对象属性标签可影响参数转发。若需按类似宿主语言的成员表达式的值类别而不是成员是否为非左值引用进行转发,需确保被转发的值不是带有临时对象标签的引用值。
操作数标签中:
- 唯一引用标签由所在的项单独决定。
- 临时对象标签仅在递归绑定时所在的所有列表项都是非引用值时包含。
原理
引用值的不可修改属性标记不可修改项而避免非临时对象的转移。这和宿主语言中的非 mutable
类数据成员访问操作符决定 const
限定符的规则类似。
子项标签继承规则保证使用 &
或 %
标记字符时,值类别的决定规则和宿主语言的成员访问操作符类似:
- 列表左值中的元素总是被绑定为左值。
- 列表右值的元素按元素是否为引用被绑定左值或消亡值。
- 特别地,项引用的临时对象标签不被继承到作为子项的被引用对象,因为即便被引用的列表对象是一个临时对象,它的元素不被作为纯右值匹配。这和宿主语言中成员访问操作符访问的右值是消亡值而不是纯右值类似。
使用对象语言,若需判断列表左值中的元素是否为引用值,可直接绑定列表操作数为引用并按需转换为消亡值再递归绑定列表元素。
部分函数合并的求值包含形式参数树,通过绑定规则在环境中引入绑定,其调用指定绑定操作。具有这样的语法构造的表达式是绑定构造(binding construct) 。
一些绑定构造使用 <binding>
提供在一个表达式多次出现的形式参数树和操作数树。
注释
按绑定初始化的约定,操作数树的子节点初始化被绑定的形式参数树的对应子节点。
和 [RnRK] 不同,各种绑定构造可使用 <body>
提供操作数。
对绑定项的处理和 [RnRK] 的其它不同参见文法元素补充约定。
另见初始化。
除类似 Kernel 的常规绑定外,NPLA1 的部分绑定构造支持延迟附加的绑定的形式。
强递归绑定支持若同时绑定的递归符号值构成循环引用,则递归绑定的值都是未指定的内部表示而不引起错误。
强递归绑定是对象语言的绑定构造实现的附加机制,形式参数树的递归匹配仍使用递归绑定。
绑定构造可支持参数转发(argument forwarding) ,根据实际参数确定形式参数中是否为引用值,保留值类别和可修改性。
注释 类似宿主语言中的转发引用参数。
若其中存在副作用,其顺序还满足:
- 若存在同一形式参数树子节点的不同绑定的操作,则这些操作的副作用之间非决定性有序。
- 不同符号值的形式参数树子节点的绑定操作的副作用之间无序。
- 形式参数树的子节点上的绑定操作的副作用先序所在的节点上的绑定操作的其它副作用。
原理
这些规则允许并行的深度优先遍历的绑定实现。深度优先遍历使任一时刻成功完成绑定的对象集中,相对其它策略其状态更易预测。
关于对象的存储,基本内容参见 NPLA 存储和对象模型。
另见对象语言内存安全保证。
NPLA1 的对象是一等对象。由定义,NPLA1 的对象默认确保同一性。
例外参见 NPLA1 子对象。
对象的引用值通常不保证其作为被引用对象和其它对象都不同一,包括唯一引用的情形。但除非另行指定,作为函数实际参数的对象若是右值引用,则实现可假定被引用对象唯一。
注释
关于右值引用的保证类似 [ISO C++] [res.on.arguments] 。注意这在对象语言而非宿主语言中适用。
基本内容参见 NPLA 子对象。
子对象可具有引用值,即子对象引用。
子对象引用访问的被引用对象不保证具有同一性。
原理
和宿主语言不同,通过相同方式构造的子对象引用访问的被引用对象未指定是否为同一对象。
这允许实现使用和宿主语言不同的方式创建非一等对象作为子对象的表示。
注释
和宿主语言不同,NPLA1 对象语言不直接提供访问子对象的内建语法。
作为使用名称表达式访问对象的推广,特定操作可使用非环境的其它对象显式地访问其子对象。
除非另行指定,这些访问操作以本节约定的规则确定结果的值类别和类型。
若指称非环境对象的表达式 E1
是访问操作的(被求值的)实际参数,子对象来自这个参数指定的对象;否则,子对象来自 E1
引用的环境对象中的被绑定对象。
具体的访问操作确定具体的被访问的子对象。
访问操作中:
- 若指定结果是引用值,或指定(在结果中)保留引用值且被访问的对象实际存在可被保留的引用值,则结果是被访问的子对象的可能经引用折叠的引用值。
- 否则,结果是被访问的被访问的子对象复制初始化的值。
- 此时,按对象的可转移条件判断复制或转移。
访问操作的结果的值的类型和值类别满足一一对应,且结果经值类别转换后和被访问的子对象的类型相同。访问操作中没有明确指定的结果的值类别以此通过结果的类型推断。
若通过以上约定,仍没有明确结果的值类别,则按以下默认规则确定:
结果是具有被访问的子对象类型对应的值,它的更具体的值类别通过上述等价关系按结果的类型对应确定。
被访问的子对象访问若具有影响值类别或被传播以外的其它属性被保留,对应在结果中出现。
成员访问(member access) 操作访问称为对象的成员(member) 的子对象,满足本节的约定。
具体操作可具有其它改变结果的值类别和类型的约定而实际使用不同的规则。
原理
确定结果的值类别和类型的方式类似按宿主语言的成员访问确定对表达式 E1.E2
的值类别和类型。E1
不一定是左值。
NPLA1 没有形如 E1.E2
的特设对象访问表达式语法,而以具体的访问操作代替,因此可具有近似但不同的规则。
特别地,除非 E1
引用环境,在 E1.E2
中显式指定被访问的子对象的表达式 E2
在访问操作中一般并不存在。代替这里的 E2
的是由具体访问操作指定被访问的子对象,其类型直接代替 E2
的类型。
其中,按有序对访问列表的最后一个元素时,被访问的子对象不是引用值,即视为纯右值。这里不蕴含求值,不会有值类别转换。
通过被访问的子对象的类型和 E1
的值类别确定结果的值类别的默认规则类似 C++ 成员访问表达式 E1.E2
确定值类别的规则,但略有不同:
- 因为不保证存在名称表达式
E2
,不需要求值算法使用类似 C++ 的unqualified-id
一致的方式使结果总是左值。 - 被访问的对象是右值引用值时,结果是右值引用值(即消亡值),而不一定是左值。
- 和 C++ 不同,NPLA 消亡值总是右值引用类型,NPLA 纯右值也此类似 C++ 纯右值实质化转换初始化的消亡值,因此逻辑上需要 C++ 消亡值的情形和此处的 NPLA 右值近似。
注释
初始化非引用值的复制初始化(包括以下的替换消亡值为右值)可能通过返回值转换实现。
在对象表示上,传播引用值的不可修改属性决定 E1
指定的被访问对象或被访问的子对象若具有不可修改属性,结果也具有不可修改属性。
典型地,被保留的其它属性包括临时对象引用。临时对象引用可被继续绑定而可实现按需转发被引用对象。
按默认规则访问相当于这些规则确定的值类似 C++ 表达式 std::forward<decltype(E1.E2)>(E1.E2)
的值。
具体操作可具有其它改变结果的值类别和类型的约定而实际使用不同的规则。
例如,推断结果的值类别的规则中的左值和消亡值可被替换为右值,则近似 C++ 表达式 std::forward<std::remove_cvref_t<decltype(E1.E2)>>(E1.E2)
。
传播引用值属性和宿主语言及递归绑定规则类似。
关于子对象的修改,参见对象的修改和改变。
对象作为实体可修改和改变,可具有可变数据状态及可变管理状态。
NPLA 约定的表示同宿主环境的对象,其修改也同这些对象的修改。
隐藏状态在针对对象语言的的讨论中被排除。除非另行指定(由具体操作的语义蕴含),所有可变状态都不属于这些被排除的状态。
改变对象可引起诊断:
以下状态是可变管理状态:
- 环境中的被绑定对象。
除非另行指定,其余可变状态都是可变数据状态。
类似宿主语言(如关于 const
限定符的语义),生存期开始前或结束后的(可能并未完成构造的)对象中的子对象的修改不是对象的修改;对应地,此处的子对象的变化也不是对象的改变操作。
改变上述的被排除的状态的修改操作不被视为对象语言中的对象的改变操作。
对包含所有权的子对象的修改是对所在对象的修改。
除非另行指定,NPLA1 不限制任意对象不可修改。
等价关系和限制不可修改性的方法的方式不唯一,因此不可修改性也不唯一。
因为外部表示不唯一,不需要基于此定义一种正规的关于外部表示的等价判断形式。
对象的不保证同一性的子对象的修改和改变不保证蕴含对对象的修改和改变。
原理
开放类型映射不保证非特定对象之间的不可修改性具有唯一的定义。
对象的修改和改变作用在确定的对象上。
若不同的对象之间不具有同一性,则作用之间无关。因此,修改和改变作为副作用,不保证在不同一的对象之间共享。
注释
所有对对象的状态的约定针对同一个对象。
对象的子对象作为可变管理状态,使不可变对象具有允许这些状态改变的内部可变性而和对象的可变性不同。
对诊断的要求类似 [RnRK] 。
环境中的被绑定对象在仅讨论不可变性的意义外仍是数据对象。
引起对象内的可变管理状态的改变而不改变对象的操作在宿主语言可通过类的 mutable
数据成员实现,但 NPLA1 不提供特性使任意的子对象的可修改性的限制如宿主语言的 const
限定符自动传播(而一般需要使用成员访问操作),因此也不需要提供对应的类型检查修改机制。
和 [RnRK] 不同,NPLA1 支持直接修改对象,而不只是通过指定子对象关联的被引用对象的改变操作。
冻结操作是使环境对象上具有类似宿主语言的 const
传播约束的操作;和宿主语言不同,这不是静态类型系统约束。
NPLA1 的赋值(assignment) 操作专指以引用值操作数指定对象且不引起同一性改变的对象修改。
被修改的对象由赋值操作的目的操作数决定,可能是操作数对象或其引用的对象。赋值操作后,被修改对象的值来自源操作数。
操作数和源操作数相同的赋值是自赋值(self assignment) 。
除非另行指定,赋值操作不保留源操作数的值类别和可修改性。
赋值可引起源操作数对象的复制或转移,分别称为复制赋值(copy assignment) 和转移赋值(move assignment) 。
复制赋值时不会复制消除对象。若被赋值的源操作数的值在复制出错,目的操作数引用的对象不被修改。
通过对象的子对象引用修改对象的子对象不保证作用在对象上。
原理
赋值不引起同一性改变的保证和区分复制赋值和转移赋值类似宿主语言。
宿主语言中,通过源操作数的静态类型(左值或右值引用类型)明确区分两者,但 NPLA 不要求类型系统(尽管支持类似作用的元数据),两者区分实际依赖具体行为。
子对象引用不一定保证引用完整对象,而修改的副作用可能需要完整对象的信息:
- 例如,修改作为列表的子对象的有序对时需要维护保持子对象关系的内部状态,而子有序对引用若不提供所在的列表的引用,则无法实现维护状态。
- 为维持子对象引用实现的简单性,不对这类情形进行一般要求。
- 特定操作可以提供更强的保证以允许满足变化的自由。
注释
赋值操作可能伴随赋值以外的其它副作用,如转移导致的修改。
特定的赋值操作可能不支持自赋值,指定自赋值具有未定义行为或引起错误。
注意避免使用引用值作为操作数的自赋值引起循环引用:此时除非另行指定,引起 NPLA 未定义行为。
不引起同一性改变的保证和 Kernel 的赋值操作包含以特定对象进行替换(可使用项的转移实现)而使对象被修改的情形不同。
赋值不保证子对象的同一性不被改变;子对象的引用仍可能被赋值无效化。
转移可导致被转移对象的外部可见的修改。
转移不需要是直接显式求值特定的函数调用的副作用。
注释 例如,使用唯一引用初始化对象,可转移表示被引用对象的项。
被转移的对象在转移后具有有效但未指定的状态。
注释
当前实现中,当项被转移后,表示的值为 ()
。这和返回值转换等引入实质化临时对象时可能具有的转移的作用(仅在互操作时可见)不保证相同。
作为赋值规则的推论,通过转移对象的子对象引用修改对象的子对象不保证作用在对象上。但和其它修改不同,这同时被转移对象后的状态的规则覆盖。
出现在表达式中多个位置的值在实现中可共享一个对象作为内部表示。这个对象被驻留(intern) 。
当前实现不使用对象驻留,以简化存储对象的互操作。
原理
因为子对象允许通过引用值被直接修改,驻留对象创建的共享可能影响可观察行为。
因此兼容 NPLA1 语义的驻留要求排除可修改的操作,且被驻留的值对应的对象的同一性不被外部依赖。
注释
驻留的对象在实现上共享存储,但仍区分同一性。
一般地,驻留仅适合不可变对象,或改变后提供不同副本区分同一性的可变对象。
[RnRS] 等不可变的符号可被驻留,但没有特别要求。
[R7RS] 明确要求空列表的唯一性。和驻留一致,这可实现为全局共享对象。
作为间接值的派生实现,对象语言中的引用值的无效化包括以下情形:
注释
对项的重绑定或赋值仍可能因为对子项的修改蕴含被替换的对象的销毁,引起子对象的生存期结束,而使其表示的对象的引用值无效化。
NPLA1 不要求支持任意类型的集合表示不相交,即分区(partition) 。
但除非另行指定,基于实体元素文法引入的类型仍被分区。
原理
不要求分区这避免全局地假定类型全集的具体表示,并支持开放的类型映射。
NPLA1 的类型谓词是一元谓词,只接受一个参数,以强调语言提供的接口的正交性。
注释
列表类型只包括真列表。
不要求分区、类型判断谓词、列表类型的设计都和 Kernel 不同。
本节指定在 NPLA1 允许以一等实体被使用的基本元素。
NPLA 一等对象是 NPLA1 一等对象。
注释
部分设计原则和规则和 Kernel 不同。
另见对象语义。
NPLA1 语义中对广义实体的构成依赖的使用也被称为引用,这不限被对象语言中的引用值表达。另见环境引用。
NPLA1 明确允许不通过对象的引用保存对象,但是也允许使用对象引用;即对象和对象的引用都可作为一等对象。
这也允许子对象直接被所在的对象蕴含。
引用值在创建时即引用在生存期内的对象。
注释
引用和 Kernel 及 Scheme 的引用类似。
明确允许不通过对象的引用保存对象和 Kernel 不同详见实体语义。
除非另行指定:
原理
按 [NPL