概述

  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 DarlingtonNPL (New Programming Language) 没有直接渊源;特别地,后者的多个等式的函数定义语法和高阶类型没有被内建支持,而静态类型和纯函数式限制被避免。

  Kernel 语言的原始参照实现是 SINK 依赖 MzScheme version 103 的解释器实现,和 [RnRK] 有一定差异。例如,字面量 #ignore#inert%ignore%inert 代替。

  klisp 是 Kernel 语言的一个更完善的实现。

  有些特性(如复数支持)都没有在这两个中实现提供,而仅在 [RnRK] 中指定。

实现

  在 YFramework/NPL 提供一些参考具体语言实现。当前 YFramework 主要使用抽象语言实现 NPLA 的具体派生(derived) 的实现 NPLA1 ,在这个基础上用于不同的目的,如程序配置动态加载的 GUI 等。

  NPLA 提供了比大多数现有的程序设计语言更强大的一般抽象。这集中体现在:

  • 和 NPL 的原始设计一致,不提供也不要求区分实现的阶段(phase) 。
  • 支持一等环境不修改现有语言的求值算法即可实现共享类似语法的新语言。
  • 允许以一般手段表达求值和未求值表达式的差异。

  这意味着 NPLA 是本质上动态的语言,但和一般语言不同,用户可以很大程度上动态地替换现有语言实现,包括在运行时替换一个解释实现为一个或多个优化编译器。这也意味着语言设计上既不需要区分解释实现和编译实现(本质上不对立),也不需要区分动态和静态(因为随时能从基础语言上构造出静态子集)。

  这样的特性设计在绝大多数语言中不存在并且几乎无法支持。已知唯一的例外是 Kernel ,在这些特性上有极大的相似,尽管实际上基本特性是独立设计的,并且在基本设计哲学上有极大不同( NPL 和 [RnRK] 中明确的 guidelines有很大不同且基本不兼容)。不过,考察设计的完整性,NPL 的派生语言也从中借鉴了一些重要的设计:

  • $vau合并子(combiner) /应用子(applicative) /操作子(operative) 等术语。
  • 绑定构造中支持模式匹配的形式参数树
  • 一些以合并子形式提供的操作。
    • 一般的作为接口提供的合并子在 NPL 中仍称为函数;合并子是作为表达式的函数的特定的求值结果
      • 注释 在 Scheme 中,合并子中的应用子对应过程
    • 相似的操作主要体现在名称语义上。因为一些基本设计的差异,不保证完全兼容。
    • 相似操作的实现不尽相同,但其中不通过宿主语言的直接的实现(称为派生(derivation) )有一部分几乎完全相同。
    • 使用 $ 作为一些标识符(如 $lambda )的前缀是独立设计的巧合;现在含义已和 Kernel 一致,表示 special form

  一些值得注意的和类似语言的主要设计差异(原理详见开发文档):

  • NPL 和 Kernel 类似,强调一等对象,但含义有所不同。此处的“对象”和 [ISO C] 及 [ISO C++] 中的定义类似,具有比 Kernel 更严格的含义。
    • 和 Kernel 合并子及 Scheme 过程类似,NPLA 默认使用按值传递参数和结果;但与之不同,不隐式对实际参数别名,不共享对象。
  • NPLA 和派生实现的语法和整体的求值类似 Scheme 和 Kernel 大多数基于 S-表达式的 Lisp 方言,但有一些显著的区别。
    • 和 Scheme 不同,而和 Kernel 一致,NPLA 避免顶层(top-level) 和局部的上下文的差异。
    • NPLA1 明确区分约定包括列表项的求值规则。和传统习惯不同,NPLA1 中括号明确不需要表示应用的含义,这可以减少一些场合(如命令行)需要输入的过多的连续括号。
    • 和 [RnRS] 定义的 Scheme 以及 [RnRK] 定义的 Kernel 一致,不支持某些 Lisp 方言的方括号 [] 替代圆括号 () 的语法。
    • 不提供注释语法。
    • 语言实现中内置预处理处理中缀 ;, ,作为前缀合并子 $sequencelist 的语法糖。两者的含义和 Kernel 中的相同(类似 Scheme 的 beginlist )。
  • 和 Kernel 相似而和 Scheme 不同,使用操作子及一等环境和 eval 代替 Scheme 的卫生宏(hygienic macro)(en-US) 及宏展开的作用。
    • 和 Kernel 类似,鼓励使用直接求值风格而不是引用(quote)
    • 不过 NPLA 也提供了 $quote 的派生而非如 Kernel 一样完全避免。
  • 和 Kernel 不同,NPL 明确支持资源抽象,不保证支持循环引用,而 NPLA 明确不支持循环引用。
  • NPLA 明确支持基于 [ISO C++] 实现的对象模型和互操作,且明确不要求支持全局 GC
    • 从在互操作的目的出发,和 C++ 具有相似性和相容性。
      • 支持基于 C++ 抽象机语义的更一般的副作用(除原生 volatile 外)和一等状态(first-class states) 。
      • 和 [ISO C++] 类似,在语言规则中保留特定的未定义行为,而不要求实现避免。
      • 暂时不直接支持多线程环境,但可以在不同线程上同时使用不同的实现的实例。
      • 函数默认使用不隐式别名的按值调用和返回传递复制或转移值,和 C++ 对应上下文的复制初始化(copy initialization) 语义一致。(不过求值为操作子的 NPL 函数在 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

领域设计原则

  本节描述被本文档中的一些原理讨论引用的的公共依据。

  原则指关于设计和实现的哲学,同时作为一般规则约束设计和实现的工程阶段。

  关于需求特别是通用目的语言的讨论,参见需求概要(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”.

  — ISO/IEC TR 18015

最小接口原则

  在满足需求的前提下,尽可能使用符合倾向减小实现需求代价的单一良基关系下具有极小元的接口设计。

注释 减小实现需求的代价,如减小设计工作量。

  这是一条模式规则,依赖具体情形何者符合良基关系的极小元这条非模式规则作为输入。

  实际使用时,非模式规则可以直接指定为二元关系的子集,或者一种良序的度量。

注释 例如,“公开函数声明数”“模块数”。

  这个输入也可能直接对应符合需求集合的某种最小功能集合而不需要附加度量,如表示某种设计的裁剪。

  注意规则指定的基数是对实现需求有意义的代价,因此不涵盖避免不必要付出的代价

  在确定的范围内尽可能少地提供必须的接口,避免不必要的假设影响接口适应需求的能力,同时减少实现需求的各个阶段的复杂性。

  适用于一般需求的实现,特别地,强调“通用”目的时。

  对需要在计算机上实现的人工语言设计:设计语言不应该进行功能的堆砌,而应该尽可能减少弱点和限制,使剩下的功能显得必要。

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]

注释

  其它各个领域中的实质等价一些表述包括:

关注点分离原则

  关注点分离(separation of concerns, SoC) 原则 :局部设计的内容应和需求的陈述或其它隐含的设计前提分别一一对应。

  适用于一般需求的实现,特别是其中依赖认识论观点的过程。

原理

  这条规则利用需求和设计内容陈述中概念外延普遍蕴含的局域性(locality) ,提供给定代价下更多的可行性或求解给定问题时使用较小的代价,用于:

  • 应对不可控复杂条件下使问题可解。
  • 局部可复用现有解的子集。

  此外,尽管并非总是必要,应用知识内容的简单假设、最小接口原则和本原则可在认识论上导出还原论。

形而上学

  根据作为需求的价值观,归纳适用于通用目的语言应有的构成及其性质(形而上学(metaphysics) )的设计规则,包括三条价值判断实现公理:

  • 设计应尽可能满足正确性
  • 设计应尽可能满足简单性
  • 设计的正确性应优先于简单性。

  具备这些性质的设计可视为由价值判断蕴含,预设前提为真的设计方法论的实现。

注释

  注意和 worse is betterthe 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 :

  • G1a 对象状态(object status) :语言操作一等对象
  • G1b 可扩展性(extensibility) :用户定义的设施能重现内建特性能力(capability)

  以上原则在 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 相似的核心设计:

规格说明

  在附录之前的以下章节给出 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) 作为输出。
  • 运行:运行生成的最终目标代码。

  其中分析阶段是任意实现必要的,依次包含:

  • 词法分析(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 ,若满足:

  • 以 L 为宿主语言的嵌入实现,不包含扩展执行阶段
  • 单一实现不保证提供多线程执行的支持,但对资源的使用进行适当的分组,以允许多个实现同时在宿主中多线程执行。

  宿主语言提供的实现环境称为宿主实现环境,简称宿主环境(host environment)

注释

  若支持多线程执行,需要附加的显式同步。

  这种实现可能提供宿主多线程对应的实体,其中包含需要的被隔离的资源。

  其它语言的实现也可能提供类似的设计,例如 V8 的 v8::Isolate

  另见可移植互操作意义上的宿主环境

基本文法

  本章约定基本的 NPL 文法规则中,包括语法及对应的基础词法。对应的语义在下文列出

  多态文法规则:派生实现可完全不提供本章明确定义的词法和语法构造的支持,仅当提供同构的替代文法且符合语义规则。

基本文法概念

  • 字符(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) ,不在被复合的表达式之间存在其它记号。

  同一个表达式可能被按原子表达式出现的位置以不同的方式规约为复合表达式。允许的规约复合表达式的方式由派生实现定义。

列表表达式

  文法:

<list-expression> ::= <left-list-bound> <expression>* <right-list-bound>
<left-list-bound> ::= ( | <extended-left-list-bound>
<right-list-bound> ::= ) | <extended-right-list-bound>

  列表表达式是在其他表达式的序列(可能为空)左右附加一组 <left-list-bound><right-list-bound> 作为边界构成的表达式。

  <left-list-bound><right-list-bound> 是不同的标点。

  边界为 () 的表达式是基本列表表达式。其它可能的边界由派生实现定义,构成扩展列表表达式。

注释

  列表表达式的边界是 NPL 图形分隔符

名称

  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 公共语义

  NPL 的语义规则构成演绎系统(deductive system) (en-US) ,通过对翻译单元中的表达式求值表达。

  除非派生实现另行指定,仅使用表达式指定关于对象语言中的计算的语义。

  基本语义规则要求:

  NPL 允许程序具有语义等价的未指定行为。派生实现可能通过约定和限制其具体选项的选取以指定更具体的实现行为

基本语义概念

  • 区域(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) 的集合。
    • 重写规则指定重写(rewrite) :接收项输入并产生作为重写的输入的项,和被重写的项之间满足某种等价关系重写关系(rewrite relation)
    • 重写规则集合以包含语法元素和语义变量的重写关系在元语言中表达为模式(scheme)

  语法上蕴含自由变量的项是开项(open term)闭项(closed term) 是开项在项上的补集。

  为表达计算,限制特定的重写关系使之不满足自反性,得到规约关系(reduction relation) ,即指定规约。对应地,双向的重写规则限制为其子集的单向的规约规则(reduction rule) 。经限制的系统是项规约系统(term reduction system)

  规约关系视为表达计算查询(computational query) 的项和答案(answer) 的项之间的映射。此时,项规约系统被作为一种计算模型

  • 注释 为表达计算的答案的确定性,需要确保规约可能取得范式

  一般地,项规约系统关联的结构总称为演算(calculus) 。

  对每个演算,存在和项对应的上下文(context) 。元语言中,一般的上下文以语义变量 C 表示,形式化为具有元变量(meta variable) □ 的以下构造:

C ::= □ | ...

  其中 ... 是演算支持的项的语法中替换子项得到的对应构造。

  一般的项记作语义变量 T ,则 C[T] 表示上下文 C 中作为元变量通过语法代换(syntactic replacement) 为项 T 的结果,它是一个项。

  作为对象语言的变量的项可依赖不同的上下文指称不同的实体。

  一个变量 x 被上下文 C 捕获(capture) ,若对任意 x 是其自由变量的项 TT 中自由出现的 xC[T] 中是约束变量

  • 注释 C 中仍可因自由出现的 x 而使 xC[T] 中的自由变量。

注释 例如,对作为对象语言的 λ 演算,语义变量 x 表示约束变量,其上下文为:`C ::= □ | (CT) | (TC) | (λx.C) 。

状态和行为

  状态不变蕴含语言规则中或可选地由实现定义的等价关系决定。

  除非派生实现另行指定,约定:

  • 实现行为总是可使用状态进行描述。
  • 存在副作用可观察(observable) 行为的必要条件。
  • 在实现外部访问某个状态的操作(输入/输出操作)是副作用。

  若存在状态等价性以外描述的行为描述,由派生实现指定。

  可观察行为如有其它外延,由派生实现指定;否则存在副作用是存在可观察行为的充分条件。

  实现应满足实现行为和语义蕴含的可观察行为等价。除派生实现指定的更特定的具体行为等价性外,其余的行为等价性未指定

  实现可支持实体具有对外部不引起可观察行为差异的隐藏状态(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) 的理论中满足最小依赖原则,尽管其实现仍可能依赖序关系。

  等价谓词的用途和上下文相关。

  同一性在作为等价关系蕴含实体上的任何其它等价关系。被蕴含的等价关系可具有更多的限制条件。

  实体的同一性是普遍的,但不是普适的,它仍不足以在所有上下文中都被关注。

  同时,同一性无法保证在对象语言的所有实体上被实现;否则,会引起根本上显著的问题,限制语言的可扩展性,而和通用目的语言的一般属性冲突:

  决定普适的等价谓词中蕴含的统一的等价关系是不可能的,因此语言中应允许共存多个等价谓词。具体等价谓词的设计可由派生实现及语言的用户提供。

  等价谓词设计中弱化等价性的一个例子是 [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) 的状态;若可能取得实体的值,其值未指定
  • 析构性转移(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) )的情况。

  更重要地,并非任意引用的共享都不改变程序的语义和行为,不合理的使用可能造成非预期的作用。

  任意地引入共享引用而使用户不便预测其作用破坏适用性

  区分是否需要表达共享的情形一般不能由语言实现预知。和使用全局所有者的问题类似,使对象默认共享的设计若需避免违反避免不必要付出的代价,在此相对不默认共享引用的设计违反简单性

  默认共享引用可能是隐式的,即语言的实现不通过程序代码中的显式标注的操作而引入共享的引用,且往往无法保证通过一等对象上的操作避免被引用的对象被其它一等对象引用——无法使用对象语言的操作排除共享引用(即便是新创建的对象也没有保证,尽管实现上不必要)。

  在要求一等对象都是引用的设计中,一般地,只有不要求名义同一性的非对象的实体才能安全地共享引用,但在非对象实体上的类似引用的机制并没有保证通过一等引用提供为语言特性。

  其它情形中,允许引用之间的隐式的共享使不相同的对象可能共享状态而破坏同一性的行为保证:程序无法可靠地避免共享状态导致的对可观察行为的影响,此时共享状态的改变非预期地影响其它对象,其行为不具有一致性

  为了排除破坏同一性和适用性的问题,语言的设计需要限制引起问题的操作的可用性(例如,[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 value copy) :创建值的副本进行替换的严格求值。
      • 引用传递(pass by reference) :使用引用替换的严格求值。
    • 共享对象传递(pass by shared object) :使用的共享机制以及对象和值或引用的关系由派生实现定义。
    • 部分求值(partial evaluation) :允许求值分为多个阶段(phase) 分别进行。
  • 非严格求值:
    • 正规序(normal order) :以最左最外(leftmost outmost) 优先的顺序求值。
      • 最左的顺序的意义同应用序。
    • 按名传递(pass by name) :使用名称替换且保持作为名称的表达式最后被替换的求值。
    • 按需传递(pass by need) :按名传递但允许合并作用相同的表达式。
  • 非决定性求值:
    • 完全归约(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) 可替代一等续延

原理

  协程可视为是在计算上可表达性等价的一次续延

  • 参见这里
  • 其中,对称协程类似一次无界续延,非对称协程类似一次有界续延
    • 对称协程可通过非对称协程补充操作实现。
    • 类似地,有界续延可通过添加显式的界限实现无界续延
    • 但是,这种功能相似不表示一一对应。
      • 注释 参见以下关于出让操作使用续延实现的讨论。
  • 在核心语言支持存储(store)可修改一等状态副作用的前提下,非对称协程和对称协程在可表达性上等价。
  • 一等续延和协程在一定条件下可互相实现。
    • 对称协程可实现一次续延。
      • 注释 另见这里,但这个实现没有检查续延调用内部可能的非预期重入,且不满足 [Hi90] 中的 PTC 要求
      • 因此,作为一等对象时,协程可作为一等续延的替代实现方式。
    • 非对称协程的出让操作可通过无界续延实现。
      • 虽然可实现出让操作的续延捕获并非续延调用,但续延调用对控制的转移不必然蕴含区分调用和调用者。
        • 事实上,使用 call/cc 捕获续延创建的是无界续延。
      • 有界续延可实现无界续延,因此出让操作也可使用有界续延实现。
      • 注释 另见这里

  典型的设计中,函数表达式默认创建例程,而协程使用特设的语法标记过程得到。特设的关键字(如 yield )提供语法,对应非对称协程中的出让操作。

注释

  关于过程的参数和过程调用之间的计算顺序,参见求值策略

λ 抽象

  λ 抽象(lambda abstraction) 是 λ 演算中的基本构成之一,提供匿名函数。

注释 λ 抽象的语法包含的形式是典型的操作符。

  在原始的无类型 λ 演算中,λ 抽象不支持蕴含副作用,子表达式求值顺序任取而不改变范式的存在性和值。

  在使用热情求值的语言中,λ 抽象创建的过程是应用合并子

vau 抽象

  Vau 抽象(vau abstraction) 是 vau 演算 ([Shu10]) 中的基本构成之一。

  Vau 抽象创建的过程是操作合并子

注释 使用 vau 抽象可实现引入 λ 抽象的操作符,如 [RnRK] 提供的 $vau 操作合并子。

函数合并

  具有操作符和操作数的项的组合可被特定的方式进行规约。这种组合是函数合并(function combination) ,包含:

  • 具有至少一个约定位置的子项 E1复合表达式 E ,当且仅当 E1 是被求值作为操作符的函数时,E函数合并表达式(function combination expression)
  • 其它具有操作符和操作数的项是非表达式形式的函数合并。以下操作符和操作数记作 E1E2

  以下规则中,非表达式形式的函数合并也可被视为表达式求值。

  求值函数合并时,子项 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] 而和 [RnRS] 不同,因为函数合并可接受非真列表作为参数,非列表的操作数可以和非列表的续延实际参数直接对应。
      • 因为函数合并的这种性质,续延关联的应用子的应用和续延应用存在直接的一一对应关系。
  • 但是,为避免和简单性冲突,[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][Shu10] )称为 λI 演算,不支持表达 K 组合子
    • [Bare84] §2.2 指出 λI 演算具有的一些问题,如:
      • 对应的理论 λI 翻译到组合子逻辑的理论 CL 时,项能取得范式的性质不被保持。
      • 范式的概念过于侧重语法,所以在模型中不确定含义。
      • 试图识别编码偏函数(partial function) (en-US) 需要的“未定义”的项是不可能的。
      • λI 演算定义的偏函数的组合对应的项不一定是 λI 演算定义的被组合的偏函数的项的组合。
    • [Bare84] §2.2 指出,这些问题都来自 [Chu41] 选择用无法取得范式的项编码“未定义”的概念。
      • 定义可解性(solvability) ,以不可解代替不能取得范式编码“未定义”可解决这个问题。
        • 项的可解性定义为存在有限的项序列使前者在后者顺序应用得到 I 组合子(即 λx.x )。
      • 在 λI 演算中,不可解等价不能取得范式。而在 λ 演算中,不可解等价不能取得 HNF
      • λ 演算没有 λI 演算的上述问题。
      • λI 表述的 Church–Turing 论题仅限于全函数,而 λ 函数表述的论题能扩展到一般形式的偏函数。
    • 即便不考虑上述整体性质,尽管计算上 λ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) 多态:仅对项上局部的项上的类型作用使之满足上下文兼容要求的多态:
    • 函数重载(overload) :同一个名称对应的不同的函数实体,允许按实际参数的类型选择调用不同的函数。
    • 强制(coercion) :求值时使值向某个上下文要求的类型的隐式转换。
  • 参数(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

  当前维护的主要派生语言为 NPLA ,是 NPL 的抽象语言实现派生实现

  NPLA 的参照实现 NPLA1 是具体语言实现,约定特定于当前参照实现的附加规则和实现。

  作为原型设计,NPLA 重视可扩展性。

  作为 NPL 的派生实现,NPLA 对象语言的设计遵循 NPL 符合性规则,并满足如下要求或附加限制。

注释

  NPLA1 是 NPLA 的一个派生实现。

NPLA 领域语义支持

  • 位(bit) :表示二进制存储的最小单位,具有 0 和 1 两种状态。
  • 字节(byte) :基本字符集中一个字符需要的最少的存储空间,是若干位的有序集合。
  • 八元组(octet) :8 个位的有序集合。

NPLA 整体约定

NPLA 实现环境

  NPLA 使用宿主语言为 [ISO C++11](及其之后的向前兼容的版本)的简单实现模型 NPL-EMA

  以下要求和宿主环境一致:

  • 字节占用的位(至少占用 8 个二进制位)。
  • 作为事件顺序的在先发生和在后发生和宿主语言中的定义一致。
    • 注释互操作,一般应避免和之后的(受实现支持的)[ISO C++] 版本冲突。

  NPLA 实体的内部表示是宿主语言中可表达的数据结构。

  NPLA 实体的外部表示是宿主语言中可通过输入/输出操作处理的数据。

  除非另行指定,NPLA 使用宿主语言提供的异常作为异常执行机制

  除非另行指定,程序不使用使宿主语言区域指定的行为(locale-specific behavior) 改变的特性。

原理

  默认避免改变区域指定行为简化设计约定。

注释

  关于类似的对宿主语言程序的要求,另见 YSLib 项目文档 doc/LanguageConvention.txt

附加功能

  NPLA 支持数值(numerical value) ,但不要求支持具体的数值计算。

  NPLA 实现为派生实现提供数值类型和相关的操作的基本支持。

  除非另行指定,若派生实现支持数值计算,其实现兼容 NPLA 数学功能的实现。

NPLA 词法和语法

  词法分析可接受多字节文本编码的字符串形式的源代码,但不假设其编码中除 0(空字符 NUL )以外的具体代码点被编码的数值,不转换编码

  使用可选的语法预处理和 NPL-GA 语法

  字符集的约定同宿主环境

NPLA 标识符

  NPL 标识符外的以下词素也是 NPLA 标识符:

NPLA 扩展字面量

  NPLA 扩展字面量包括:

  • #+- 起始的但不全是 +- 构成的、长度大于 1 的词素。
  • 十进制数字字符起始的词素(当被支持时)。

  全由十进制数字字符的词素表示十进制数值。派生实现可定义其它作为数值的词素。这些词素作为字面量时,是数值字面量(numerical literal)

NPLA 名称和字面量求值

  名称仅被实现为和字符串的一个真子集一一对应的表示(参见类型映射)。

  除非派生实现另行指定,只有代码字面量不是自求值表达式,其余字面量都求值为右值

  代码字面量求值时解释为名称。

  数据字面量是自求值的字符串的外部表示

  数值字面量是自求值的数值的外部表示。

  存在不保证先求值的子表达式语法形式特殊形式(special form)

  特定的名称是保留名称(reserved name)

  除非另行指定,在源代码中使用保留名称作为实体的名称的程序行为未定义

NPLA 求值的表示

  规范形式是特定类型的 [ISO C++] 对象。

  名称解析失败可被忽略而不终止实现演绎;保证名称表达式求值的强规范化

  不要求提供命名空间实现的可变实体。

  不保证求值都是纯求值;非特殊形式使用热情求值;其它情形使用热情求值或惰性求值的方式由具体特殊形式约定。

  对象语言的函数默认为过程,过程默认实现为子例程。过程指定的计算结果和函数表达式最终求值结果关联过程调用结果的恒等映射。

注释 即过程调用的结果总是同函数值。

  除非另行指定,实现函数的宿主数据结构生存期要求默认同宿主语言

  除非另行指定,按值传递支持复制初始化对象的一等作用

原理

  NPLA 函数不支持类似 [ISO C++] 的类型退化(decay) 。作为动态类型语言,需要被转换的值在操作内部实现,不需要在返回值上另行附加转换。

  按值传递的复制初始化和宿主语言的对应语义类似。

NPLA 类型系统

  NPLA 使用隐式类型而非显式类型

  NPLA 使用潜在类型具有类型;不指定动态类型以外的类型

  显式类型(如清单类型)的机制可由派生实现指定可选地引入。用户程序也可能添加类型标注和不同的类型机制的支持。

  除非派生实现另行指定,引入的静态类型应和动态类型一一对应。

  NPLA 使用和宿主语言相容的动态类型检查。除非派生实现另行指定或类型映射的需要,使用的类型检查规则和宿主语言一致。

  宿主语言对象的值描述状态,且宿主语言要求的对 volatile 左值的操作属于可观察行为

NPLA 互操作支持

  NPLA 的宿主语言应能提供 NPLA 及派生实现的本机实现

  NPLA 的派生实现提供特定的和宿主语言的互操作支持,可其中和 NPLA 提供的关于互操作的具体行为不同的部分应由实现定义。

注释 对派生实现,NPLA 约定的具体默认互操作特性是可选的。但是,一般的约定如开放类型系统仍被要求。

  NPLA 和派生实现可约定互操作的具体实现的要求,以确保实现的状态可预测。

  本机实现可以具有 C++ 的实现兼容的二进制接口的函数提供,这些函数称为本机函数(native function)

  本机实现可直接支持本机函数在实现中被调用。若被支持,具体接口由派生实现指定。

  本机函数作为函数的实现,其调用的求值可具有和非本机的函数一致的作用,但不需要具有可被对象语言表达的函数体

  为确保函数求值的作用可能保持一致,本机函数应符合和本机函数调用时使用的规约一致的方式使用,即在宿主语言的意义上至少符合以下规约调用约定

  • 被调用时的子项被作为以 WHNF 形式表示的被调用的表达式使用。
  • 调用后具有项被重写为必要的值以表示函数调用返回值

  本机函数的返回值应能表达任意的非本机函数调用的返回值,即通过求值函数调用中函数体的非本机函数的求值结果

原理

  实体的内部表示和外部表示满足实现环境的要求允许在宿主语言程序中直接实现关于表示的操作,简化了互操作机制的设计和实现。

注释

  宿主语言自身的调用约定(通常和实现的 ISA 相关)作为 C++ 实现自身的 ABI ,在此是中立的,没有提供特设的支持的要求。

  另见 NPLA 基础存储和对象模型

类型映射

  类型映射(type mapping) 指定对象语言和宿主语言之间的实体类型之间的关系,是前者中的类型到后者中的类型的映射。

  作为类型映射目标的宿主语言类型或其子类型称为宿主类型(hosted type)

  作为宿主语言类型的宿主类型是典型的。其它宿主类型是非典型的。

  具有特定动态类型的对象语言的在宿主语言具有宿主类型,以宿主语言的值表示,称为宿主值(hosted value)

  在互操作的意义上,宿主值在作为对象语言的值的表示中以宿主对象(hosted object) 的形式被保存并可在宿主语言中访问。

  对象语言的值被对象语言的实体类型表示蕴含它被映射的宿主类型表示,反之亦然。

  类型映射可以是非空的多对一、一对多或一一映射。

  若类型映射是一一映射,其类型等价性同宿主语言的语义规则;否则,由类型的语义规则约定。

  因需提供与作为宿主语言的 [ISO C++] 的互操作支持,所以明确约定实现中部分实体类型对应的 C++ 类型:

  • 用于条件判断的单一值的宿主类型是 bool
  • 字符串的宿主类型都是 string 类型。
  • 和字符串的子集一一对应的词素的宿主类型是能映射到 string 的另一种类型。

注释 string 是占位符,不要求是和 [ISO C++] 的 std::basic_string 相关的类型。但一般地,string 类型应具有和 std::string 相近的操作以便实现对象语言语义及支持互操作。

  推论:字符串和词素可直接比较相等性或排序。

  NPLA 数值在对象语言中具有数值类型,具体类型映射未指定,但在 NPLA 数学功能提供可选实现。派生实现可显式扩充或替换定义其它数值类型的类型映射。

  其它宿主类型由实现定义。具体宿主类型参见以下各节和对象语言类型对应的描述。

  宿主类型在对应的 C++ API 中可能以类型别名的形式引入。

原理

  类型系统开放的,可能提供不被对象语言支持的宿主语言类型和值。

  但符合已指定的类型的实体需能被视为同种类型的实体使用,即子类型。

注释

  非典型的宿主类型可以是特定的宿主类型的值的子集,即便这样的类型不被宿主语言的类型系统直接表示。

  不被对象语言支持的值的一个例子是实现使用的中间值(thunked value)

  关于中间值、string 类型的具体要求、NPLA 数学功能的规格说明和由实现定义的命名空间,参见 YSLib 项目文档 doc/NPL.txt

NPLA 未定义行为

  一般地,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 一等对象类型

  除类型映射,NPLA 约定能作为一等对象的类型支持的抽象的类型,作为实现的最小要求的一部分。

  以下章节扩充 NPLA 的其它类型,这些类型中的一部分可能作为一等对象。

  基于开放类型系统,派生实现可定义其它类型,不论是否被互操作支持。

原理

  这些类型在求值算法等规则的描述中适用。

有序对

  两个不同对象可作为元素(element) 构成有序对(ordered pair, pair)

  有序对的元素是子对象

  当且仅当若有序对的两个元素不同,交换元素得到的有序对和原有序对不同。

注释

  一些编程语言中,构造有序对的操作称为 cons ,有序对又称为 cons 对。

广义列表

  列表(list) 一种类型,它的对象可能具有子对象

  空列表(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 允许派生实现引入实体的规则不受限制。

NPLA 基础存储模型和对象模型

  因需提供宿主语言互操作支持,除不支持静态(static) 存储和没有提供支持的存储操作外,NPLA 的基础存储模型和对象模型和 [ISO C++11] 相同。

  当前不支持的存储操作包括分配函数(allocation function) 取得的存储和线程局部(thread-local) 存储。

  NPLA 还允许类似对象具有未指定的存储或不需要存储的实体,以使一等实体可涵盖宿主语言在功能上等价的非对象类型(如 C++ 的引用)。这些实体若被支持,其存储实现和互操作接口由派生实现定义。

  NPLA 中不是一等对象一等实体仅由派生实现定义。

  保证存储性质的差异不被依赖时,不区分一等实体和一等对象的实现方式。

  在此情况下对象都是固定(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 临时对象的存储未指定,但部分临时对象被项所有

  求值结束而不被使用的项的资源在求值终止时被释放,包括被项独占所有权的这些临时对象。

  求值终止包括可被实现确定的异常退出。

  对名义上被项所有的临时对象,必要时实现可分配内部存储转移项(包括在环境中分配),以满足附加要求(如生存期附加约定)。

  对象的所有权随可随对象被转移,参见对象的复制和转移

  求值结果可以是:

  • 作为值计算的结果的一等对象,称为结果对象(result object)
  • 传递异常的状态的实体。
  • 派生实现可定义的其它实体。

注释求值规约,其它的求值结果的存在未指定,若存在则可能需要其它处理,可能依赖和处理一等对象的值不同的语义规则。

  函数调用时以活动记录保持被引用对象的所有权。活动记录及其帧的具体结构、维护方式和生存期由派生实现定义。

  除非另行指定,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 API
  • 调用 NPLA 中其它取对象内部表示的值的间接值使之被修改的 API 。

  排除非内存安全操作以及非内存安全的本机实现,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 环境

  求值环境维护名称和作用域。

  变量名(variable name)变量名称

  除非另行指定,环境维护的名称都是变量名。

  NPLA 的求值环境可以是:

  • 一等环境(first-class environment) ,即作为对象语言中的一等对象的环境。
  • 作为 NPLA 非一等对象环境记录(environment record)

  环境可引用若干个关联的其它环境为父环境(parent environment) ,用于重定向

  除非派生实现另行指定:

  • 环境可引用有限多个父环境,其数量的上限未指定。
  • 父环境在创建时指定,作为实体,之后不可变

原理

  如需求中指出的,本设计避免命名空间隔离,因此只有一种被环境支持且被求值算法统一处理的名称。

  若派生实现需要,可修改环境的内部表示和求值算法的名称解析步骤以对不同的名称添加支持。相反,在已有多种名称的设计中还原为一种设计是更复杂和不可行的。因此,在本设计中不预设多种名称。

环境对象

  环境作为可保持可变状态的对象,是环境对象(environment object)

  环境对象包含变量名到表示被绑定实体的映射,称为名称绑定映射(name binding map) ,实现变量绑定

  被绑定实体是对象时,称为被绑定对象(bound object) 。NPLA 环境对象中的被绑定实体包含一等对象,因此被绑定实体总是被绑定对象。

  环境对象对其中的名称绑定映射具有独占的所有权。名称绑定映射对其中的对象可具有独占或共享的所有权。因此,环境对象可对包括被绑定实体的名称绑定映射中的对象具有独占或共享的所有权。

  环境记录之间共享所有权,以环境引用访问。

  环境对象是名称解析时查找名称的目标

  父环境可共享环境记录。通过共享环境记录实现重定向的环境表示是链接的(linked) 而非平坦的(flat)

原理

  仅在可证明符合语义要求等价时,使用平坦的环境表示。

  对支持一等对象语义的设计,因为明确要求区分同一性,对象的存储不能被任意地复制。

  一般地,仅在可证明父环境对应的环境记录在对象语言和实现内部都不被共享访问(不具有共享引用且不被别名),且不存在任意派生实现定义的对释放顺序引起的可观察行为差异时,才能唯一具有这个父环境的环境为平坦的表示而保持语义不变

注释

  变量名通过以和字符串一一对应的值表示,没有直接的值的限制,可能为空串。

  若环境记录直接持有被引用对象,则这些对象是环境记录的子对象

环境引用

  环境引用是对象语言中访问环境记录的一等对象

注释 环境引用不是引用值。后者关联的被引用对象是一等对象。

  环境引用共享环境对象的所有权

  根据所有权管理机制的不同,环境引用包括环境强引用环境弱引用

  环境强引用可能共享环境对象的所有权,对环境对象的名称绑定映射持有的项具有间接的所有权。

  作为间接值,环境引用可被复制或转移。

  复制或转移环境引用不引起被引用的环境对象被复制。因此,按值传递环境引用不引起其中所有的对象被复制。另见引用

原理

  区分环境对象和环境引用在纯函数式语言不是必要的,因为不需要关心环境中的子对象的复制影响可观察行为

  否则,为支持影响可观察行为的环境的修改,非环境记录的环境引用是必要的。

  环境引用也是一种较简单且一般普遍高效的父环境的实现表示,可直接实现链接的环境而不需要证明和实现特设的其它内部表示能和抽象机意义上链接的环境保持语义等价。

  续延捕获若复制续延,可能引起关联的环境的复制,影响可观察行为并引起不必要的实现开销。为此,区分环境引用是必要的。

  以环境引用作为一等对象使访问被引用对象等环境记录的子对象时需要间接访问,在环境实际不需要被复制的大部分其它场景引起开销。这种开销是可接受的,因为:

  • 考虑到一等环境的普遍性,有必要有效支持对象语言中创建环境临时对象(而不仅仅是环境对象的引用值)的使用使之避免复制。
  • 实现可能提供附加的证明以在优化的翻译过程中替换环境引用为环境记录或其它不需要间接访问的中间表示,以消除这些开销。

  不论这样的证明是否存在,环境强引用和弱引用仍在对象语言中区分,以明确接口上的所有权语义

  引入环境弱引用作为一般的引用机制,且仅在必要时使用环境强引用,以避免过于容易引入循环引用引起强内存泄漏,符合适用性

当前环境

  NPLA 对象语言中,表达式的求值隐含对应一个环境对象作为求值算法需要的上下文输入,称为当前环境(current environment)

NPLA 表达式语义

  本节约定对象语言中的表达式相关的语义规则,特别是求值规则

  列表表达式作为一等对象列表

值类别

  表达式归类为具有以下基本的值类别(value category) 之一:

  • 泛左值(glvalue) :求值用于决定被表示的对象的同一性的表达式。
  • 纯右值(prvalue) :求值不用于决定对象同一性(而仅用于初始化临时对象或计算对象中存储的值)的表达式。

  一个泛左值可能被标记为消亡值(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) ,其被引用对象是被另一个对象所有的、作为这个对象的子对象的一等对象。

  子对象引用对特定操作可表现和其它一等对象不同的行为。

  以下引用是子对象引用:

  • 子有序对引用(subpair reference)子有序对作为被引用对象的引用。
  • 子列表引用(sublist 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 中而不在运行时可访问的元数据中(事实上,也不在对象的动态类型中)保留。关于其应用,参见进一步讨论

注释

  因为宿主语言的引用折叠,以上 PP&& 一致。

  被绑定的这些对象可作为临时对象引用关联的被引用对象

  另见绑定操作

临时对象的表示

  作为一等对象的临时对象和其它一等对象表示方式一致。

  非一等对象临时对象包括:

  对临时标签对象决定的非一等对象,去除临时对象标签后,应具有一等对象表示。

  在项引用以外的临时对象标签仅在被绑定对象上存在。

  关于一等对象表示,参见 YSLib 项目文档 doc/NPL.txt

原理

  至少在逻辑上,临时对象作为对象语言中不可见的对象和一等对象相同的宿主类型(即项)作为内部表示。因此,区分其内部表示并非通过宿主语言中的类型,而需通过运行时性质确定。

  一些表示可能仅出现在临时对象中,而不是合法的一等对象表示。实现可据此进行一定运行时检查,以排除互操作或者错误实现中的误用。

  对临时对象标签决定的非一等对象和一等对象表现之间的要求简化实现的一些操作,使实质化不需依赖另行分配的资源。

  临时对象的子对象不是临时对象,简化对临时对象的一些操作,也减少可能使临时对象标签扩散(到非预期的上下文影响一等对象表示)而误用。

  关于被绑定对象的规则,参见绑定临时对象中的原理。限制标签的使用范围以使之不和其它表示冲突。

  关于实现中项的宿主类型和简化实现的例子,参见 YSLib 项目文档 doc/NPL.txt

注释

  直接构成项的非一等对象可以是通过源代码中的外部表示翻译变换得到的具有内部表示的数据结构的非一等对象,参见上述实现中项的宿主类型

表达式的类型

  NPLA 的类型系统使用隐式类型;默认使用潜在类型,保证表达式的值具有类型

  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)

尾调用和 PTC

  在尾上下文规约的调用尾调用(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 不要求延长生存期,仅要求特定情形下缩短生存期,且其它情形被释放的对象生存期不延长到尾上下文外。

  允许延长生存期是生存期附加约定的结果。

PTR

  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

  TCO(Tail Call Optimization ,尾调用优化)是在以尾上下文规约时,允许减少修改规约状态的优化。

  一般地,TCO 可重新排列规约过程中的被语义允许调整的副作用和其它不影响可观察行为的状态的调用,减小空间开销。

  TCO 的这种性质可以在宿主语言不支持 PTC 时用于实现对象语言的 PTC

注释

  关于 TCO 和 PTC 的差异,另见这里

宿主语言中立

  C++ 不要求实现支持 PTC ,也不保证支持 TCO 。因此,对象语言的 PTC 要求显式的 TCO 实现。

  为可移植地支持 TCO ,NPLA 不依赖宿主语言中不可移植的互操作的活动记录(通常是体系结构相关的栈)。

注释

  尾调用可避免尾上下文中非嵌套调用安全的情形的宿主语言实现的未定义行为,但不保证非尾上下文中具有类似的性质。

TCO 实现策略概述

  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 数学功能

  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)

  对应地,<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]+ :十进制整数值。
    • 不论符号,当前精确数数值字面量默认都具有宿主类型 int ,除非其绝对值太大而无法被表示,使用其它类型代替。
    • 除非精确数字面量的数值超过所有 fixnum 的可表示范围,都具有 fixnum 值。
    • 除非精确数字面量的数值超过所有支持的精确数的可表示范围,都是精确数。
    • 注释 当前精确数的表示范围是 fixnum 中宿主类型的值域的并集,因此超过 fixnum 可表示范围的数值不是精确数。
  • 匹配正则表达式 (+|-)?[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 的不同精度:
        • Ee :默认精度。
        • Ss :短(short) 精度。
        • Ff :单精度(float) 。
        • DD :双精度(double) 。
        • Ll :长精度(long) 。
        • 同组的字母含义等价。以上精度中,默认精度不低于双精度,其它精度依次不低于之前的一个。
      • 精度可影响内部存储的宿主类型。
    • 若字面量指定的数值小于或大于使用的类型的数值表示范围,则值为对应类型具有相同符号的零值或无限大值
  • 匹配正则表达式 (+|-)(inf|nan)\.(0|f|t) :带符号的 flonum 特殊值。
    • 其中,inf 指定无限大值,nan 指定 NaN 值。
    • 后缀指定精度:
      • 0 :默认精度。
      • f :单精度。
      • t :扩展(extended) 精度。
      • 以上精度中,默认精度不低于双精度,扩展精度不低于默认精度。

  除非派生实现另行指定,以上要求外的数值的子类型和内部表示未指定。

原理

  浮点数解析存在不同精度的算法。

  若以二进制浮点数和经过舍入的十进制表示相互转换不损失精度为前提,宿主语言的 std::numeric_limitsmax_digits10 位十进制数字足够表示。

  (对 [IEC 60559] 的二进制浮点数情形的证明参见这里的 Theorem 15 。)

  但是,对任意有效输入的结果误差都不大于 1 ULP(unit in the last place) 的不经舍入的值完全精确值(full precision) 的精确解析算法,对实现的要求较高,且性能可能明显较低,故不作要求。

  (对 [IEC 60559] 的二进制浮点数的情形,需要数十倍的中间存储,参见这里。)

  和宿主语言的 std::strtod 不同,允许使用宿主语言中的任意浮点数舍入模式,而不要求不同浮点数舍入模式下的结果一致性。

  浮点数精度的 floatdouble 在典型实现中的内部表示格式同 [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] 的设计,但没有明确指定字面量词法规则。其中对精度的表述略有不同:

  • 没有指定大写字母。
  • 指定 sfdl 的精度递增,没有显式允许不同的精度映射到相同的内部格式。
  • 没有显式允许用户指定的默认精度。

  但是,SINK 和 klisp 实际上都不符合前两点,而更符合 Scheme 的实现。[RnRK] 在此可能不完善或表述有误。

  当前 klisp 的实现不允许 Ee 之前没有小数点,且在存在 Ee 时省略之后的指数的任意部分,和 SINK 以及 [RnRS] 都不同。本设计遵循后者。

  当前 NPLAMath 精度对应的宿主类型指派如下:

  • 非特殊值:
    • e :同 d
    • s :同 f
    • ffloat
    • ddouble
    • llong double
  • 特殊值:
    • 0double
    • ffloat
    • tlong double

  指定特殊值的精度的词素语法兼容 [Racket] 。

NPLA1 核心语言

  NPL 是独立设计的,但其派生语言和其它一些语言有类似之处;这些语言和 NPL 方言之间并不具有派生关系。但为简化描述,部分地引用这些现有语言规范中的描述,仅强调其中的不同。

  NPLA1 符合 NPL 和 NPLA 的语言规则,其实现环境还应提供本章起的其它程序接口。

  互操作中的一些接口处理的值可约定宿主类型。但这些类型不一定在对象语言层次上稳定,可能在之后的版本变化。稳定性由具体实现提供的附加规则(若存在)保证。

  NPLA1 和 Kernel 语言(即 [RnRK] )的特性设计(如 vau和作为一等对象的环境表达过程抽象)有很多类似之处,因此许多概念是通用的;但从设计哲学到本章介绍的各个细节(如默认求值规则)都存在深刻的差异。

  部分名称指定的操作和 [RnRS] 或 klisp 指定的类似。

  以下章节主要介绍和 Kernel 约定不同的设计。各节的通用约定不再在之后的各个接口单独说明。

NPLA1 对象语言约定

  NPLA1 仅使用宿主语言类型作为在对象语言可表达的状态

  在 NPLA 的基础上,NPLA1 要求对象语言支持以一等对象作为表达式并被求值

  类型等价性基于类型映射及其实现,由 [ISO C++] 的语义规则定义。

  值等价性由宿主环境== 表达式的结果定义。

  除非另行指定,所有类型的外部表示都是允许从作为内部表示的项节点确定的同宿主类型的空字符结尾的字符串(即 [ISO C++] 的 NTCTS )。

  关于作为表达式的求值和类型映射的实现,参见 YSLib 项目文档 doc/NPL.txt

标识符附加规则

  当前仅支持标识符作为名称

  部分名称是保留名称:含有 $$ 的名称保留给宿主交互使用;含有 __ 的名称保留给 NPLA1 实现。

  在 NPLA 规则的基础上,在内部表示中显式使用保留给实现的标识符的程序行为未定义

注释 这包含在源代码以外的中间表示使用的情形,但不包含作为用户输入的数据。

NPLA1 互操作约定

  基本规则参见 NPLA 互操作支持

  非 NPLA1 实现提供的类型的宿主 == 操作不要求支持嵌套调用安全

  作为 NPLA1 嵌套调用安全的约定的扩展,若存在 == 操作不支持嵌套调用安全的类型,具体类型由派生实现的定义。

  对象语言操作和互操作不修改对象语言中已经可见的一等环境父环境

原理

  NPLA1 中提供的类型仍需要支持嵌套调用安全,以满足嵌套调用安全的约定中的要求。

  关于 NPLA1 嵌套调用安全的具体约定和其它实现原理,参见 YSLib 项目文档 doc/NPL.txt

  避免修改已在对象语言可访问的一定对象的父环境符合同 [RnRK] 的环境封装性对对象语言的要求。这允许实现假定仅在有限的上下文中父环境可修改,而减少优化实现的难度。

NPLA1 程序实现

  本章指定 NPLA1 对象语言的核心语言特性。包含库特性的其它设计参见 NPLA1 参照实现环境

原理

  一般的语言能支持不同实现形式的库,包括源程序和其它无法判断是否和特定源程序关联的翻译后的程序。

  复用这些程序时,可能需要根据不同的形式而分别处理:源代码被读取和求值而加载,而其它格式的翻译形式可能直接映射存储后经特定的检查即被加载。

  但是在可复用的意义上,这些不同的形式是一致的,都视为库。

注释

  和 [RnRK] 不同,库不限定其实现形式。[RnRK] 指定的库实质上是可使用对象语言派生实现的库。

  典型的静态语言不保证程序执行时能对源程序进行翻译,因此加载程序的限制通常更大,可能无法处理源程序形式的库而首先需要分离翻译为其它格式。NPL 一般不具有这个限制。

  关于对象语言的派生实现,参见 YSLib 项目文档 doc/NPL.txt

外部环境

  基于 NPLA 整体约定,由 NPL-EMA ,NPLA 的实现不假定存在多线程执行环境。

  但是,宿主语言可支持多线程环境执行,可引起宿主语言的未定义行为

  作为 NPLA 的派生,NPLA1 对象语言程序也具有相同的性质,除非另行指定需要和外部环境交互的特定操作,不需要假定 NPLA1 引入存在多线程执行环境。

附加元数据

  NPLA1 实现可提供和实现环境或具体 NPLA 对象关联的附加的资源,用于提供程序运行时可得到的附加信息,如源代码位置。

  是否存在这些附加元数据(extra metadata) 和附加元数据的具体内容可影响特定的行为。

注释 如符合诊断中要求的实现的具体行为。

  这些影响是未指定的,但除 NPLA1 程序直接依赖具体数据而进行的操作外,不应影响程序的其它语义(例如,引起程序终止)。

NPLA1 扩展支持

  本章中除循环引用的限制外,不支持的特性可能会在之后的实现中扩展并支持。

NPLA1 未定义行为

  NPLA1 对象语言程序中的未定义行为包括 NPLA 未定义行为和以下扩展 NPLA 未定义行为:

  派生语言可约定其它未定义行为。

原理

  扩展 NPLA 未定义行为可提供更严格的要求使实现更简化。

  关于环境的一些未定义行为可视为违反内存安全,而不需要单独实现。

接口文法约定

  为描述对象语言规则和程序接口,本节约定文法形式。

注释 这仅用于描述接口,不依赖 NPL 语言的基本文法

  规约操作中项的约束通过以 <> 中的同类名称表示。

  为区分同类约束的不同项,约束的名称后(在 > 之前)的可带有以 1 起始的正整数序数。除非另行指定,这些序数仅用于区分不同的同类约束项,无其它附加含义。

  本节描述的项是被用于求值(参见求值算法)的或它们的直接文法组合。前者应能涵盖原子表达式、其求值结果以及预期在对象语言中实现对象语言求值算法所需的 NPLA1 用户程序构造。

  库可参照本节的方式约定通过项的文法,以支持仅在特定库使用的操作数。

  除非另行指定,本节的对应要求同时适用于本节中和这些库中引入的项。

元文法基本约定

  元语言文法:

  • ... :Kleene 星号,重复之前修饰的项 0 次或多次。
  • + :重复之前修饰的项 1 次或多次。
  • ? :重复之前修饰的项 0 次或 1 次。

注释

... 一般在结尾出现,表示元素构成列表。

  和 [RnRK] 不同,不使用 . 分隔有序对,不使用元素名称的复数表示列表。

实体元素文法约定

  指定具名的函数的文法中,第一项以符号值的形式在所在的环境中提供,指定求值结果指称为合并子的函数的名称;其后指定的文法中不同的元素对应合并子的操作数或其被作为调用时的形式参数树的子项。

  除非另行指定,在操作数可能是左值时,仅当对应以 ...? 形式中最后的一项(若存在)时,支持匹配作为被引用对象的有序对的非前缀元素不是空列表的引用值的情形。

  名义不同的约束可能蕴含相同的检查

  除非另行指定,应用子的操作数的约束也适用其底层合并子

  文法形式上,使用本节约定指定应用子的操作数时,指定表达式形式的求值结果。

注释 这和 [RnRK] 和 [Shu10] 中的斜体标识符的标记不同,但含义(表示语义变量(semantic variable) )和效果实质相同。

  操作数可能是左值或右值,按具体操作的需要,在必要时可被转换。

  除可能具有的子类型关系,本节约定的不同类型的操作数构成的集合之间不相交。一般规则参见类型分类

  根据是否可作为操作子中指定不被求值的函数参数,本节的操作数及其子项分为未求值的操作数求值得到的操作数

原理

  约束可用于区分特定的含义,但不直接指定和具体的检查对应,以便被实现优化,例如合并名义不同的检查。

  文法形势的匹配应避免歧义。

注释

  底层合并子适用应用子的约束,意味着按求值算法,被求值的函数合并对象不能是非真列表

  对非前缀元素的支持和绑定匹配规则对应。其中:

  • 结尾序列支持匹配以空列表以外的值的引用值作为非前缀元素的有序对操作数左值的被引用对象中的非前缀元素子对象
  • 非结尾序列的元素因计算前缀元素数而被要求在同一个对象的前缀元素中。
  • 作为操作数被绑定时,若元素是引用值:
  • 若有序对操作数的非前缀元素是空列表的引用值,则有序对操作数构成列表。
  • 关于操作数匹配的规则避免匹配操作数序列时对文法元素的对应关系可能具有歧义。

未求值的操作数

  未求值的操作数的文法约定如下:

  • <symbol>符号
    • 注释 内部使用和 <string>一一对应的表示,不提供符号和外部表示的其它映射关系。
  • <symbols> :元素为 <symbol> 的列表,形式为 (<symbol>...)
  • <eformal> :表示可选提供的环境名称的 <symbol>#ignore ,或这些值的引用值。
    • 注释 通常为动态环境。
  • <expression> :待求值的表达式。
    • 注释 这是 NPL 语法的直接实现。作为右值,它是词法元素的,或这些元素的真列表
  • <expressions> :形式为 <expression>... 的待求值形式。
    • 求值时,<expressions> 被作为单一表达式(即视为求值 (<expression>...) )。
  • <binding> :绑定列表的元素,形式为 <symbol> <body> ,用于指定被求值的表达式和绑定参数的符号值。
    • 和 Kernel 不同,<symbol> 后不要求是整个 <expression>
  • <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> )。这些上下文中总是隐含了上述的可派生实现的要求。
  • <body> 不蕴含顺序求值子项。
    • 原理 这也允许 <body> 中的表达式被整体求值

  关于合并子基本操作,参见 YSLib 项目文档 doc/NPL.txt

原理

  和传统 Lisp 方言(包括 [RnRS] 和 [RnRK] )的函数体不同,<body> 的各个表达式之间不蕴含顺序求值。

  因此,和 [RnRK] 不同,$vau 不需要在基本操作之后再次派生。这使操作的功能更加正交。

注释

  和 [RnRK] 不同,<symbols><definiend><formals> 具有一些附加的约定支持;<body> 不蕴含顺序求值子项;NPLA1 的符号可通过代码字面量求值得到。

NPLA1 对象语言语法

  基于 NPLA 基本语法约定参见 NPLA 整体约定

  NPLA1 表达式符合 NPL 表达式语法

注释

  NPL-GA包含的转义规则中包含 <char-escape-seq><char-seq> 要求类似 [R6RS] 在字符串中的元素;其中仅有 \ 和 " 被 [R5RS] 直接支持,而 [R7RS] 不支持 \v 。

  后者支持的其它转义字符序列词法可被派生实现以 <$literal-char> 的形式另行指定(其中 [R7RS] 可涵盖对应 <char-escape-seq> 的功能)。

NPLA1 字面量

  基于 NPLA 词法规则,本节指定字面量的词素集合。

  派生实现可指定不同的字面量,但不应和已指定词法构造的记号冲突,包括本节指定的字面量。

  NPLA1 字面量都是纯右值,但总是允许实质化转换为消亡值并引入允许互操作临时对象

注释 这和宿主语言的字符串字面量是左值不同。当前 NPLA1 对象语言不提供能引起互操作差异的接口(字符串字面量不被修改),以后可能改变。

字符串字面量

  字符串字面量的类型为 <string>

NPLA1 数值字面量

  基于 NPLA 数值类型数值字面量,NPLA1 数值字面量的类型为 <number>

  除非另行指定,数值的具体宿主类型未指定。

注释 部分数值可指定具体的子类型

  NPLA1 支持 <integer> 类型的精确数数值字面量和 flonum 不精确数数值字面量。

  支持的字面量包括词素符合 NPLAMath 数值表示的字面量。

  派生实现可定义其它数值字面量。

注释

  以上字面量包含十进制数值的字面量。其它字面量是 NPLA 扩展字面量

  无限大值和 NaN 值同 [Racket] 的字面量词法,除这些类型总是启用,且使用明确属于 flonum 且对应明确宿主类型的 long double 代替不被作为一般 flonum 的 extflonum 。

  [SRFI-73](已撤消)提出扩展 [R5RS] 的带有 #e#i 前缀的精确数和不精确数无限大值字面量,其中前缀及精确数的支持和 [RnRK] 类似。NPLA1 不支持无限大值精确数。

  关于无限大值的在 [RnRS] 的一些实现情形,另见这里

NPLA1 扩展字面量

  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> 类型的布尔值,使用与这些字面量不同的对应表示。

NPLA1 函数合并

  以下使用 ...作为函数的操作数时,可支持没有操作数的函数合并。此情形下应用表达式仍需要前缀 () ,但不在以下规约文法中显式地表示。

注释

  和 Scheme 及 Kernel 不同,求值算法决定求值为函数合并表达式的语法表达不需要括号,且具有不同的函数合并形式

对象语言内存安全保证

  对象语言可能提供关于内存安全的检查。

  除非另行指定,假定实现进行互操作无法保证内存安全。

对象语言基本内存安全保证

  对象语言提供关于内存安全的基本保证:不存在违反内存安全相关的要求以外的未定义行为(包括循环引用等)、不存在不保证内存安全的互操作且不存在不安全间接值访问时,对象语言的程序执行保证内存安全。

  非内存安全操作在对象语言中以不安全间接值访问的一部分情形体现。

不安全操作

  不安全(unsafe) 操作是可能在程序的执行中引入未定义行为操作

  这里的未定义行为包含在操作中直接引入的未定义行为,以及因为操作被执行而使程序在之后无法确保排除的未定义行为。

  不安全操作是实现可选提供的。

  当前对象语言不支持并发访问对象。数据竞争仅可由和宿主语言的互操作引入。

不安全间接值访问

  对象语言的不安全间接值访问包括:

无效的环境引用

  环境对象被销毁导致作为间接值的环境引用无效化

注释

  另见环境生存期

无效的引用值

  作为间接值的引用值是间接值的实例,因此无效的间接值包含无效的引用值,通过无效间接值访问包括无效的引用值的访问。

  对象语言不提供悬空引用以外构造无效引用值的操作。

  对象语言中可引入悬空引用的情形包括:

  • 调用可能返回引用值的合并子,且没有另行保存函数体求值所在的当前环境返回值对应的对象不在生存期内:
    • 绑定到形式参数的右值保存在过程调用的局部环境中,退出函数体的求值,局部环境被释放后,返回的其引用是悬空引用。
    • 实现等效上述情形的派生操作的使用,如:

  关于间接保留引用值和互操作可能引入悬空引用的情形,参见 YSLib 项目文档 doc/NPL.txt

注释

  另见对象语言的引用值

其它无效的间接值

  使用其它不保证内存安全的操作可引入不具有内存安全保证的间接值访问实体。

  这些间接值可能因为和悬空引用相同的情形无效化

保留间接值

  对象的(直接或间接)子对象是间接值时,对象包含间接值。

  修改对象为间接值或使之包含间接值时,对象保留间接值。

  被保留的间接值是对应的通过修改得到或包含的间接值。

  本节中的概念对应适用于具体的间接值,如被保留的引用值和函数在结果中保留引用值

被保留的引用值的目标

  函数调用返回(在对象语言中允许出现的,下同)间接值或包含间接值的对象时,在函数值中保留间接值。

  函数调用修改环境使环境对象保留间接值(绑定间接值或包含间接值作为子对象的对象作为被绑定对象)时,在环境中保留间接值。

  函数调用修改一等对象或其子对象,使之保留间接值时,在对象中保留间接值。

  在函数值中保留间接值、在环境中保留间接值、在对象中保留间接值的函数保留间接值。

  被保留的间接值被函数调用的求值结果蕴含时,函数在结果中保留间接值。

注释

  函数调用的求值结果排除副作用即函数值。

  在结果中保留间接值包含以下情形:

  • 在函数值中保留间接值。
  • 在环境中保留间接值,环境是函数值或其子对象。
  • 在对象中保留间接值,对象是函数值的子对象。

被保留的引用值的来源

  函数返回包含间接值的对象由参数的值决定时,保留参数中的间接值。

  按被保留的间接值的来源,这分为以下两个子类:

  • 直接保留间接值:接受间接值参数。
  • 间接保留间接值:接受的参数或参数在特定环境中被求值得到的结果决定是否直接保留间接值。

保留间接值的操作

  操作可保留间接值:

  • 使用函数调用实现的操作可通过函数调用保留间接值。
  • 其它实现方式可等效地保留间接值。

注释

  在结果中保留间接值的操作不区分被保留的间接值的来源和目标。但多数情形下,这通过函数值保留参数中的间接值蕴含。

保留间接值和内存安全

  保留间接值操作的内存安全的一个必要条件是所有被保留的间接值在之后的使用中都满足内存安全。

  保留间接值在操作后可能因间接值无效(如悬空引用),无法继续保证内存安全。

  在环境中保留间接值时,应保证环境具有足够的生存期,以避免间接值依赖无效的环境引用导致访问环境中对象的未定义行为。

对象语言接口的安全保证机制

  对象语言接口的安全保证机制提供不同接口的分类,通过允许区分是否具有内存安全保证的接口帮助程序利用对象语言基本内存安全保证

  通过避免或限制使用不安全操作,实现上述安全保证。

  因为允许引入 NPLA 未定义行为,无法提供安全证明的互操作应视为不安全操作。

  基于求值算法的安全保证的非形式的证明框架概述如下:

  • 任意步骤中,访问间接值指定的目标对象是安全的,仅当间接值是安全的。
  • 符号值的求值是安全的,仅当引用的环境是安全的。
  • 合并子调用的求值是安全的,仅当合并子、操作数及调用的操作是安全的。

原理

  满足安全保证的推理如下:

  • 因为 NPLA 实现的非互操作引入的、非求值规约的管理规约不存在未定义行为,以上求值算法中的步骤中通过排除不安全的实体能保证规约中不存在未定义行为。
  • 因为规约决定程序执行的语义,在求值中排除不安全的实体可以保证不存在未定义行为,而满足安全保证。

安全性附加证明

  一些不安全操作是否蕴含未定义行为可能依赖具体调用使用的操作数。

  若能证明特定的前提保证任意的调用实例中的操作数满足附加的安全假设,则这些不安全操作的调用仍可保证安全。

  排除不确保安全性假设的互操作时,NPLA1 提供附加调用安全:若不存在隐藏环境中绑定的可修改对象的引用,则仅因可能违反值稳定性的不安全操作的调用是安全的。

  派生实现可对特定调用附加使用限制以便提供证明,或定义其它的调用并提供更强的保证。

诊断

  NPLA1 的特定求值步骤可引起诊断

  引起诊断时求值被终止,或在失败可被恢复时以其它派生实现定义的方式继续求值。

  其它引起诊断的条件可被派生实现补充指定。

注释 注意未定义行为取消对诊断的要求。

  本节以外的诊断消息的其它形式未指定。

注释

  引起诊断的求值包括:

  • 抽象求值的失败。
    • 例如,REPL(read-eval-print loop) 中进行的翻译。
  • 环境中访问指定名称的对象失败时。
  • 特定的函数应用。

  其它求值条件详见具体操作的规定。

NPLA1 错误

  NPLA1 中的错误是按接口的约定不符合预期的正常条件(如不被正常处理的操作数类型)引起的诊断。

  求值特定的表达式可引起错误,包括:

  • 违反求值算法步骤中的要求而直接引起的语法错误(syntax error)
  • 其它情形引起的语义错误(semantic error)

  以接口文法约定的形式约定的操作中,除类型检查外,绑定初始化之前的参数绑定失败是语法错误。

  语法错误包含两类:

  • 总是依赖程序运行时确定的值不满足特定操作的要求引起动态语法错误。
  • 其它语法错误违反语法正确性要求,是静态语法错误。

  类似地,语义错误包含两类:

  • 总是依赖程序运行时确定的值不满足特定操作的要求引起动态语义错误。
  • 其它语义错误违反语义正确性要求,是静态语义错误。

  静态语法错误可能通过语法分析从源代码决定。

  引起动态语法错误或动态语义错误依赖的值是合并子的具体实际参数的值,以及派生实现可选指定的其它的值。

  引起动态语法错误或动态语义错误的情形包括求值特定的函数应用,由具体操作指定。

  程序可通过引发(raise) 一个错误对象(error object) 指定引起诊断。

  除非另行指定,NPLA1 的错误对象不需要是 NPLA1 支持的对象,而可以仅在宿主环境中可见。

  因果性引起的错误可构成错误之间具有依赖关系。

  错误对象的其它具体形式由派生实现指定。

NPLA1 异常

  NPLA1 的当前诊断使用的异常执行机制由宿主语言支持,通过宿主语言中的异常类型区分不同的异常条件。

  NPLA1 约定的所有要求引起异常的诊断情形都是错误。

注释 用户操作引起异常不一定是错误。

  不引起未定义行为的翻译失败应抛出异常

  引发错误对象可能通过抛出异常实现。此时,被抛出的宿主语言异常对象是错误对象。被抛出的异常类型可具有被显式指定的 public 基类,这些基类应无歧义以允许宿主语言捕获。

  若存在依赖错误且引发被依赖的错误对象使用抛出异常实现,使用宿主语言标准库的嵌套异常(nested error) 机制实现依赖错误。

  当前没有提供相关操作,但抛出的宿主异常在具有表示的意义上是 NPLA1 的一等对象

  关于抛出异常的宿主类型,参见项目文档 doc/NPL.txt

运行时错误条件

  除非另行指定,实现应对以下全局的运行时错误条件按要求引起诊断。

  当实现无法提供需要的资源,资源耗尽(resource exhaustion) 。此时,引发特定的关于资源耗尽的错误对象。

  除非另行指定,上述表示资源耗尽的错误对象满足宿主语言的以下类型的异常对象:

注释 [ISO C++] 的本机实现宿主资源耗尽时,一般抛出派生 std::bad_alloc 的异常对象。这不包括本机实现无法提供资源的未定义行为

错误检查

  检查(check) 是限定成功的操作应满足的(必要非充分)条件引起诊断的操作。检查失败时要求引起诊断。

  良定义的检查应具有强规范化性质,以保证有限数量的检查总在有限的计算步骤内终止。在进行检查的上下文,实现假定检查良定义。

注释 实现不需在此之前对检查的这个性质附加检查。

  检查条件限定检查的通过或失败。除非另行指定,通过的检查没有作用,失败时总是具有作用。

注释 检查失败通常可引起副作用

  NPLA1 要求在特定上下文进行类型检查。派生实现可定义其它检查。

  函数操作的语义可单独指定检查,具体形式由具体操作指定。

NPLA1 类型检查

  基于名义类型,对象语言实现应具有语义规则指定的类型检查,以确保程序的执行符合操作的必要前置条件。

  操作的语义可要求以下的类型检查:

  实现可能添加其它不违反语义要求的类型检查。

  基于表达式的类型,对应对象语言表达式的表示实体的元素可指定操作数上明确的类型要求。

  部分实体从属于其它实体类型而构成子类型关系;部分的规约操作取得求值结果保证结果中的值可能具有的特定类型集合,这些类型也一并在以下描述中给出;其它情形不指定类型。

  规约预期符合约束。若违反由项的表示的对象的动态类型不匹配导致,则求值失败;否则,行为未指定。

  类型检查的完成应先序依赖被检查特定类型的值的访问。

  除非另行指定,类型检查和程序中的其它作用(包括不同的其它类型检查)的顺序未指定。

  类型错误引发错误对象。

  若合并子调用不接受非真列表参数构成函数合并,检查参数是真列表,即参数列表(parameter list) 。对参数列表的类型检查的完成应先序于其中任意子表达式的求值。

原理

  类型检查有助于维护程序的正确性,并及早发现编程错误。

  但是,类型检查自身存在开销;在一个阶段中集中检查类型的限制不是必要的。特别地,静态类型检查不被要求。

  这些设计同时确保程序容易在程序在实现的不同执行阶段重现相同的检查逻辑乃至直接复用其实现。

  为减小开销等目的,实现可能合并不同类型检查,而不改变程序的可观察行为

  对子表达式的求值需访问子表达式。因此,对参数列表的检查蕴含顺序要求。

注释

  一个值可被多次针对不同的对象进行类型检查。

  不同的类型检查中,对特定类型的值的访问之间没有必然的隐含联系。

NPLA1 外部表示

  外部表示若被确定,由实现和派生实现定义。

  NPLA1 不要求对象和其它实体存在外部表示,也不要求外部表示唯一。

注释

  对外部表示的存在性要求和 [RnRK] 不同。

  NPLA1 当前直接使用其它已被指定的表示规则,如互操作隐含的宿主语言对象表示。

  NPLA1 当前不提供可移植的互操作接口(包括一些基本 I/O 操作),也不约定其涉及的外部表示形式。

表达式语义

  表达式具有和语法构造不直接相关的且可能上下文相关的语义。

  部分语义不需要通过求值体现。

NPLA1 规范求值算法

  以被求值的表达式和所在的环境作为参数,NPLA1 使用以下规范(canonical) 求值算法取得表达式的求值结果

  1. 自求值:若被求值的表达式不是符号值且不是有序对,则求值结果是自身。
  2. 名称解析:若被求值的表达式是一个符号值,则被视为变量名,求值结果是它在上下文(当前环境确定的词法作用域)中变量绑定确定的对象的经引用折叠左值引用
  3. 否则: 注释 被求值的表达式是有序对。
    1. 若被求值的表达式是具有一个元素子表达式)的列表,则求值结果是这个子表达式的求值结果。否则,继续以下求值步骤。 注释 被求值的表达式是具有不少于一个元素的列表或非真列表。
    2. 若被求值的表达式第一个子表达式是空列表,则移除,并继续以下求值。 注释 起始空列表的语法用于继续求值可能不提供实际参数函数合并。具有实际参数的函数合并不一定需要起始空列表。
    3. 对第一个子表达式求值。
    4. 以第一个子表达式的值计算的求值结果作为操作符,以其余子表达式作为操作数,求值合并

  有序对以外的表达式被求值时:

  • 标识符的值是构成标识符的符号值。
  • 代码字面量的值是去除其边界的 ' 的标识符构成的符号值。 注释 代码字面量可表达直接作为标识符时不能作为符号值的词素的转义,例如 '' 是一个空的符号值;而 '#ignore'42 这样的形式允许其中的表达作为变量名,而不是字面量。
  • #t#f 求值为自身,是布尔值
  • #ignore#inert 求值为自身,具有和其它值不同的单元类型
  • 数值字面量求值为数值(numerical value)

  非空列表和代码字面量以外的对象作为表达式,都是自求值表达式

原理

  NPLA1 规范求值算法和 [RnRK] 中定义的 Kernel 求值算法(以及 [Shu10] 中定义的 vau 演算)类似,差异为:

  • 求值算法不直接约定取得 WHNF 以外的子项是否被求值,而由被调用的第一个子项决定。
  • 对符号值的求值包含对引用值的区分。
  • 要求一个子项的列表总是使用其列表元素求值。

  最后一个差异在对象语言中是实质性的,它决定列表表达式和其中的子表达式的求值总是等价。

  求值算法保持当前环境

  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> 分别表示对被分隔的序列参数进行有序和无序列表求值(替换后合并子功能对应参照实现环境中函数 $sequencelist% 求值后的合并子)。

  对分隔符的处理使用和组合顺序相反的两遍分别对 ;, 遍历替换。

  变换的不同 <expression> 的实例以相同的词法顺序在变换后的结果中被保存。

求值算法实现风格

原理

  和 Scheme 不同而和 Kernel 类似,求值通常使用显式的风格(详见 [Shu10] )而不是依赖 quote 的隐式风格;这和不需要括号的语法特性无关。

值类别和类型

  基本内容参见 NPLA 值类别表达式的类型

  特定的表达式维护可修改性

注释 这类似宿主语言的 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 的对象默认确保同一性

  例外参见 NPLA1 子对象

  对象的引用值通常不保证其作为被引用对象和其它对象都不同一,包括唯一引用的情形。但除非另行指定,作为函数实际参数的对象若是右值引用,则实现可假定被引用对象唯一。

注释

  关于右值引用的保证类似 [ISO C++] [res.on.arguments] 。注意这在对象语言而非宿主语言中适用。

NPLA1 子对象

  基本内容参见 NPLA 子对象

  子对象可具有引用值,即子对象引用

  子对象引用访问的被引用对象不保证具有同一性。

原理

  和宿主语言不同,通过相同方式构造的子对象引用访问的被引用对象未指定是否为同一对象

  这允许实现使用和宿主语言不同的方式创建非一等对象作为子对象的表示

注释

  和宿主语言不同,NPLA1 对象语言不直接提供访问子对象的内建语法。

子对象访问约定

  作为使用名称表达式访问对象的推广,特定操作可使用非环境的其它对象显式地访问其子对象。

  除非另行指定,这些访问操作以本节约定的规则确定结果的值类别和类型

  若指称非环境对象的表达式 E1 是访问操作的(被求值的)实际参数,子对象来自这个参数指定的对象;否则,子对象来自 E1 引用的环境对象中的被绑定对象

  具体的访问操作确定具体的被访问的子对象。

  访问操作中:

  访问操作的结果值的类型和值类别满足一一对应,且结果经值类别转换后和被访问的子对象的类型相同。访问操作中没有明确指定的结果的值类别以此通过结果的类型推断。

  若通过以上约定,仍没有明确结果的值类别,则按以下默认规则确定:

  • 若被访问的子对象是引用值,则结果是泛左值
  • 否则,若 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 对象语言数据结构

  本节指定在 NPLA1 允许以一等实体被使用的基本元素。

  NPLA 一等对象是 NPLA1 一等对象。

注释

  部分设计原则和规则和 Kernel 不同。

  另见对象语义

NPLA1 引用

  NPLA1 基于 NPLA 项引用支持实体的引用

  NPLA1 语义中对广义实体的构成依赖的使用也被称为引用,这不限被对象语言中的引用值表达。另见环境引用

  NPLA1 明确允许不通过对象的引用保存对象,但是也允许使用对象引用;即对象和对象的引用都可作为一等对象

  这也允许子对象直接被所在的对象蕴含。

  左值都通过引用值表示。另见一等引用表示存储和对象模型

  引用值在创建时即引用在生存期内的对象。

注释

  引用和 Kernel 及 Scheme 的引用类似。

  明确允许不通过对象的引用保存对象和 Kernel 不同详见实体语义

NPLA1 引用值使用约定

  除非另行指定:

原理

  按 NPLA1 规范求值算法,隐含当前环境直接求值名称表达式的求值结果是左值。这和宿主语言求值的 unqualified-id 在除了枚举器(enumerator) 外的大多数情形中类似。

  在此,这被约定为默认情形。其它情形需要附加的规则指定。

  特定的显式指定环境和名称表达式的操作访问环境中的被绑定对象,其求值结果可以是左值消亡值。这和宿主语言的涉及成员访问的表达式(形如 E1.E2E1->E2 )类似。环境相当于宿主语言中形如 E1 的对象表达式。

  因为类型系统的不同,类比成员访问的表达式时,忽略 C++ 的位域(bit-field) 、静态成员、成员函数和枚举器的访问规则。

  此处 E1 总是被视为左值,所以类似宿主语言的规则,结果的值类别由环境中的对象类型确定:当且仅当对象是左值时,结果是左值;否则是消亡值。

  因为 NPLA 的左值和消亡值是引用值,存在推论:

  访问不作为被绑定对象的子对象时,通常并非如宿主语言为支持推断参数类型的方式使用引用折叠,构造折叠的引用值默认不直接使用引用折叠的规则(除临时对象标签外同 [ISO C++] ),而直接由被引用对象确定。

  对访问列表中的子项构成的子对象引用,这也和宿主语言的涉及成员访问的表达式类似,除以下不同:

  • 元素是引用值时允许结果是唯一引用(而不是宿主语言的左值)。
    • 这是因为在此唯一引用指定的是结果的值的类型,而非类似宿主语言声明的右值引用类型。
  • 元素是临时对象的引用值时,允许引用值上的临时对象属性在访问中被区分(类似宿主语言以成员访问表达式作为 decltype 的操作数的结果)。

  访问被绑定对象使用引用值也满足不可修改引用属性的传播性质,避免被绑定对象被任意非预期地修改。

  其它情形是否需要满足不可修改引用属性的传播性质和具体操作相关,因此不明确要求。

  对违反不可修改引用引入的假定的修改操作要求错误避免隐式的 NPLA 未定义行为,因此引入类型错误作为违反引用值的属性引入的错误

注释

  互操作可能引入右值引用。

  访问子对象的具体规则参见子对象访问约定

  作为列表或者环境中绑定对象的一部分,引用值可能通过求值算法或对象语言提供的操作访问。这同时确定值类别。

  蕴含一次引用值提升转换的方式包括返回值转换

循环引用

  除非另行指定(如强递归绑定),对象中的循环引用引起 NPLA 未定义行为

原理

  循环引用破坏一些实现的假设而引起非预期的访问。

  不显式访问环境的操作也可能引入循环引用(而引起未定义行为),例如 NPLA1 参照实现环境 下:

$def! l ();
$def! l list% l;

  典型实现中,具有所有权的循环引用可引起资源泄漏;无条件遍历访问循环引用子对象的求值不具有终止保证

  例如 NPLA1 参照实现环境下求值以下表达式:

$let ((nenv () make-environment)) $set! nenv self nenv

  可引起被捕获的环境中存储的对象无法释放。

  能同时保证避免资源泄漏的实现引起一般意义上更根本的设计限制,因此不被使用。详见自引用数据结构和循环引用

  此外,为了避免 $lambda 等引起不经意的循环引用误用,根据易预测性原则,这些合并子构造器默认不使用强引用作为静态环境。

  若需保持静态环境的所有权,使用显式指定静态环境的构造器(如 $lambda/e )和 lock-current-environment 等。

  否则,容易引起循环引用,如以下表达式:

$def! f $lambda ()

  会相当于当前设计的:

$def! f $lambda/e (() lock-current-environment)

  此处锁定的当前环境的强引用被作为闭包的一部分绑定到当前环境中,引起循环引用。

  而求值当前设计中等价的:

$def! f $lambda/e (() get-current-environment)

  不引起未定义行为。

自引用数据结构

  因为不支持循环引用,不支持引用自身的自引用数据结构

注释 另见列表

NPLA1 环境

  NPLA1 支持一等环境

  环境对象也可能是语言中显式约定的和环境引用不同的非一等对象。

  NPLA1 的环境关连的父环境重定向使用 DFS(Depth-First Search ,深度优先搜索)遍历目标。

  环境中的一等对象是环境对象的子对象。子对象是环境,即环境子对象。

注释

  除了支持非一等对象的环境,和 Kernel 类似。

隐藏环境

  语言实现可提供非一等环境。总是不能被对象语言以一等对象访问的环境是隐藏环境(hidden environment)

  一般地,隐藏环境是某一个(非隐藏的)一等环境的直接或间接父环境(而能通过求值等间接操作被访问)。

新环境

  新(fresh) 环境是新创建的环境。

  新环境和先前的其它的(特别地,包括当前环境)不共享相同环境对象。

  除非另行指定,新环境是空环境

  创建新环境的一个例子是 vau 抽象实现过程调用

环境的稳定性

  环境在特定情形保证稳定性(stability) :一个环境是稳定的(stable) ,仅当总是可假定绑定维持一定意义的等价性,而可确保访问其中同名实体的可观察行为等价

  违反关于环境的稳定性的要求的程序具有扩展 NPLA 未定义行为

  当前要求确保的稳定性包括:

  环境中的绑定的对象可以在引入后通过对象的引用被修改(#对象的修改和改变)。

原理

  一般地,环境的稳定性要求构造环境时不能依赖非特定的动态环境(作为被名称解析访问的父环境),因为这些环境的绑定可能具有在构造环境之后确定的绑定,而不能确保环境中的名称具有可预知的含义。

  环境的稳定性简化分析程序的推理过程,也在许多上下文中允许程序更易被优化。

  从稳定的环境多次访问对象的计算作用是幂等的。这允许合并多次访问为一次而不改变程序的行为,允许具有较小的实现开销。

  环境的稳定性不易被可靠地判定甚至不可能被判定(例如,一个无法检查但可信的来源提供的环境),因此语言规则在此不要求进行检查。

  和 [RnRK] 不同,环境基本操作合并子基本操作及基于这些操作的一些派生操作操作数树构造时不检查其中的非列表项是否都为符号#ignore(而延迟到匹配时检查);匹配时不检查符号重复;若形式参数中的符号重复,则绑定的目标未指定。

  此外,NPLA1 提供单独的递归绑定符号的机制,且明确支持在操作数中同时递归绑定之前未被绑定的多个符号

  要求隐藏环境稳定允许实现共享隐藏环境作为父环境而提供标准环境

注释

  [RnRK] 的 make-kernel-standard-environment 若通过共享基础环境作为隐藏的父环境实现,也具有这里的稳定性。

  但是,[RnRK] 不提供同一性保证,也没有通过对象的引用修改被引用对象的操作,因此不需要支持不可修改引用即可保证值稳定性。

  环境对象符合默认的等价比较规则以及绑定的对象可通过引用被修改和 [RnRK] 不同。

  关于要求的环境稳定性,存在推论:稳定环境中的同名被绑定实体可证明排除通过对象的引用使其改变的副作用(如被修改)或总是具有同一性

环境生存期

  对象语言的实现提供给用户程序使用的初始环境的环境对象及其中的子对象满足:

  • 其创建先序于用户程序的对象的创建。
  • 除非提供为不满足环境的稳定性的环境中的被绑定对象,其销毁后序于用户程序的对象的销毁。

  程序引用环境中的名称时,应确保环境在生存期内。

注释

  环境中不满足稳定性的被绑定对象可能被修改且具有外部可见的可观察行为。若这个对象是一个环境的唯一强引用,则对应的环境对象在替换为其它值时被销毁。

  特别地,应注意使用函数时引入父环境的生存期。

  另见对象语言内存安全保证

环境中的绑定

  环境中的绑定的抽象不依赖对象语言中表达的引用的概念,允许直接关联一个没有引用的值。

  环境中的绑定对被绑定的对象具有所有权。除在环境中绑定中间值的不安全操作,这种直接所有权是独占的。

  绑定的变量名符号值构成的名称表达式解析的结果总是左值

注释

  环境中的绑定不依赖引用以及绑定所有权和 Kernel 的设计不同。

  另见一等引用绑定构造

重绑定

  环境中允许变量以相同的名称被重新绑定,即重绑定(rebinding)

注释 和 Scheme 类似。

  被绑定对象引用不因其引用的对象被重绑定操作替换值而被无效化。

  重绑定替换被绑定对象的值,不改变对象的同一性。若其中存在子对象,则子对象被销毁,任何子对象的引用值被无效化

  特别地,若继续访问已被求值指称的引用值引用的对象,则超出生存期访问而引起 NPLA 未定义行为

  任意隐藏环境 e 应满足以下绑定有效稳定性:通过引用值间接访问 e 中绑定的对象时绑定保持有效(蕴含不被移除或重绑定),保持被绑定对象的生存期和 e 对其所有权

  这避免因为上述访问违反内存安全而引起 NPLA 未定义行为。

  关于无效化,参见 YSLib 项目文档 doc/NPL.txt

被绑定对象的值和可观察行为

  任意隐藏环境e 的任意同一被绑定对象 o 应满足以下的值稳定性(value stability) :若 o 上发生使其改变的副作用(如被修改),则之后在以 e 或任意以 e 作为直接或间接父环境的环境中直接以名称解析o 的引用值访问 o 时,o 的值和发生作用前的 o 的值在影响可观察行为的意义上等价。

  若不满足值稳定性,访问副作用发生后的对象引起扩展 NPLA 未定义行为

  以下情形使对象改变的副作用不受值稳定性要求的约束:

注释

  通过限制引用值不可修改可以维护被引用对象的值稳定性。

  对象间接访问具有内部可变性的对象的可变管理状态的一类典型实例是一等环境中的绑定中的子对象(即便这个一等环境对象是隐藏环境中的子对象)。

冻结

  环境可进行冻结(freeze)冻结的(frozen) 环境中取得的绑定和引用值不可修改

  特定的环境修改要求环境不在冻结状态以确保不变量,要求类型检查。检查失败则引起类型错误

  冻结一个已被冻结的环境没有作用。

注释 冻结环境是幂等操作。

  NPLA1 隐藏环境是冻结的。

  当前 NPLA1 对象语言不提供在已有环境撤销冻结或在冻结的环境中添加、移除绑定或重绑定的方法。

  若程序中使用其它方法(附加初始化或提供本机实现操作)撤销冻结或在冻结的环境中添加、移除绑定或重绑定而使对象语言安全性保证失效,这种方法应由派生实现定义,否则程序行为未定义

  关于对象语言安全性保证,参见 YSLib 项目文档 doc/NPL.txt

原理

  环境的冻结操作类似 [ECMAScript] 的对象的冻结操作。类似地,冻结环境不会冻结其中的变量绑定中可能存在的环境子对象

NPLA1 广义列表

  NPLA1 的广义列表真列表或者无环的非真列表,其元素不构成

  列表的引用构成其它对象时,也不构成

原理 排除环使处理列表的操作不需要考虑一些复杂的自引用情形。

  绑定构造形式参数树是可能是符号或真列表。

注释 形式参数树可作为表达式直接在源程序中表达。

  通常意义的列表即真列表

  除非另行指定,NPLA1 列表类型指真列表。非真列表的类型是有序对

原理

  和 Scheme 及 Kernel 不同,NPLA 支持的列表都是真列表。另见关于自引用数据结构和循环引用的分析

  列表的这些特性确保基于列表的数据结构在对象语言逻辑上的简单性。也因此 NPLA1 对应的操作中,没有对环存在性的检查。

  没有环的结构能保证所有权语义能按需嵌入(embed) 到列表中,即列表可保证表示为同构的具有对节点所有权的嵌套 cons 对

有序对的子对象和子对象引用

  有序对的元素是有序对的子对象,有序对对作为元素的表示具有所有权。同一个有序对的元素节点之间没有所有权关系。

  部分操作可能修改有序对的子对象。

  除非另行指定,有序对的子对象被转移,使用项的转移。被转移的子对象在被转移后不在被转移的有序对中存在。

  子有序对引用可被绑定构造引入。

  关于子对象被修改和转移,参见 YSLib 项目文档 doc/NPL.txt

原理

  有序对的子对象是表示它的项的子项和值数据成员对应表示的对象。

  对象被转移后通常其子对象不需要再被访问,此时保持子对象的同一性和转移前的状态一一对应缺乏意义。作为析构性转移,使用项的转移可以复用已使用类型擦除或其它的间接存储方式持有的子对象,减小不必要的开销。

  作为默认规则,明确要求项的转移,而不是未指定是否使用项的转移,以满足语言规则自身的简单性易预测性。这类似 [WG21 P0135R1] 引入强制复制消除(madatory copy elision) 对 [ISO C++] 的规则起到简化作用。不同的是,因为没有静态类型的限制,子对象的类型能在程序运行时改变,而不需要引入静态分析开销,这同时使实现也更简单。

注释

  子有序对引用子列表引用子对象引用

  当前有序对引用总是引用至少一个有序对的元素。

  由绑定使用引用标记字符的非递归绑定的规则,绑定列表的子对象引用不直接共享操作数有序对对象,而共享元素是原容器元素的(经折叠的)引用值的有序对,是子有序对引用。

  这类似宿主语言的容器对象一般不能转换为共享容器部分元素的 C++ 对象引用。

NPLA1 合并子

  除非另行指定(如强递归绑定),对象语言中的所有合并子都是真合并子

  NPLA1 对象语言不提供其它合并子的普遍操作。

注释互操作意义上的 NPLA1 API 可支持其它合并子。

  合并子和操作数组合构成的函数合并是一个 NPLA1 对象,称为函数合并对象(function combination object)

注释 求值算法可接受的函数合并对象是有序对函数合并表达式作为函数合并对象是列表

  NPLA1 的合并子使用包装数(wrapping count) 存储可能需要求值操作数的次数。

  在不出错时行为和不使用包装数而直接使用嵌套子对象实现的行为完全一致,但在到达实现支持的最大包装数时继续包装即包装数溢出(wrapping count overflow) ,行为可能不相同:

  若某个操作使合并子超出上限,则符合非宿主资源耗尽的错误条件

  实现支持的最大包装数应满足:若发生包装数溢出,则直接创建和包装数相同个数的合并子符合宿主资源耗尽的错误条件

原理

  为可修改性,允许非真合并子。这可在互操作中表示类似合并子但在语言中不可见的非一等对象。

  为维护语言规则的简单性,合并子默认是真合并子。

  NPLA1 对象语言不提供其它合并子的普遍操作,这不保证完全满足类似 G1b 的原则。

  尽管没有要求,这种规约也更符合 G1b ;同时,这易于移植 Kernel 代码。

注释

  Kernel 的合并子对应 NPL 的真合并子

  对最大包装数的要求需要实现支持包装数是能和宿主资源的空间相较规模的值,这保证使用包装数的实现的空间效率不弱于不使用包装数而直接分配合并子包装的实现。

  这也表示通常用户程序的操作不会发生包装数溢出:若包装操作的次数导致包装数溢出,则直接分配合并子的替代操作也应由于宿主资源耗尽而失败。

NPLA1 数值

  数值支持的实现兼容NPLA 数学功能

  NPLA1 数值字面量求值结果是数值。

函数的间接值使用约定

  引用值作为间接值,首先符合作为实体的引用的使用约定。

间接值作为实际参数

  除非另行指定,一般地,函数接受左值引用操作数,使用引用的对象的值和直接使用右值作用相同,但不会修改被左值引用的对象。

  这等价隐含无副作用的左值到右值转换,被视为蕴含左值到右值转换。

注释

  另行指定的例子如函数参数转发

  此处的左值引用和宿主语言中的( constvolatile )左值作用类似。

间接值作为函数值

  部分函数值总是非引用值。

  这些操作对应的函数调用返回非引用值。

  返回非引用值的行为应等价返回值转换

  其它操作可具有引用值结果,对应的函数调用可返回引用值。

原理

  函数值非引用值可满足具体操作的语义要求(如非引用值的构造器),减少误用的可能性,并帮助提供内存安全保证

保留引用值

  保留间接值,包括直接保留间接值和间接保留间接值,适用间接值引用值的情形,对应地称为保留引用值、直接保留引用值和间接保留引用值。

  除非另行指定,被保留的引用值不被折叠

原理

  必要时要求引用折叠可避免引入非预期的引用的引用值。

  被保留的引用值可能逃逸或不逃逸而通常不能直接证明具有内存安全保证

保留环境引用

  保留间接值适用环境引用

注释

  和保留引用值的情形不同,因为只允许通过环境引用在对象语言中访问环境对象及其子对象,访问环境但不保留环境引用的操作只可能在(不保证内存安全的)互操作中出现

函数参数和函数值传递约定

  函数可能接受引用值参数返回引用值,是对函数的形式参数或函数值的初始化

  在复制初始化形式参数和函数值时,部分函数保证被初始化的值和初值符值类别和可修改性一致。这些初始化是转发操作。

注释

  另见函数参数和函数值传递

传递非引用值参数

  一些函数的参数进行左值到右值转换,实现参数的按值传递

  这类似宿主语言中直接使用对象类型的形式参数。

函数参数转发

  一些求值为引用值的函数的部分实际参数被保留,而不进行左值到右值转换

  这些值以保留值类别不变的形式被直接作为操作数,用于调用其它合并子。这种参数被转发

注释

  这些参数的转发类似绑定构造支持的参数转发

  参数转发的实现可判断值类别后分别对传递非引用值或直接传递引用值提供实现,或直接使用绑定构造。前者支持本机实现。

返回非引用值

  返回非引用值和参数的按值传递类似:若初始化函数值的初值符是引用,复制或转移被引用对象的值而不是引用值。

注释

  这类似宿主语言中返回 auto 类型。

函数值转发

  一些其它保留引用值的操作中,引用值来自参数,且难以通过自身的逻辑单独决定可否安全地直接返回引用值。

  此时,在返回之前根据特定参数是否为引用值,可选地转换函数值以确定是否保留引用值,即进行转发。

  特定的显式转发操作转发临时对象引用值使临时对象被转移,以转发的值作为结果,可不同于使用返回值转换

  • 同返回值转换,转发转移右值,复制左值。
  • 但当转发临时对象可确定唯一使用时,也转移临时对象。

原理

  函数值转发使某些操作在默认情况下满足间接值生存期规则而保持内存安全,符合适用性原则

注释

  确定是否保留引用值的机制类似 [ISO C++14] 中从没有括号的 id-expression 上推断返回 decltype(auto) 类型是否为引用类型。

  函数值转发的实现可通过判断是否需要转发引用而按需决定返回引用值或非引用值,或使用标准库的相关函数。前者支持本机实现。

  另见对象的可转移条件

  显式转发操作把右值、消亡值和带有临时对象属性左值引用视为被转发的目标。

  转发列表对象的子对象可能转移这个对象。

创建和访问对象的函数

  构造器(constructor) 是用于创建对象的函数。

  除非显式指定创建的对象具有引用值类型,构造器是典型的(typical)返回非引用值

  部分操作涉及对其它对象具有所有权的对象。

  一部分对象的构造器创建的对象完全通过其它对象的引用或对象的值作为构造器的参数而决定,且创建的对象对这些参数具有所有权,这样的对象称为容器(container) 。

  容器构造器的参数作为容器的子对象,是容器的元素(element)

  以容器对象或其引用作为参数,取得容器元素对象或其引用的函数是容器元素访问器(accessor)

  标准库提供一些属于构造器和访问器的操作。除非另行指定,标准库的访问器符合子对象访问约定

注释

  容器的元素扩展了有序对和列表的元素的概念。

  一些不是容器的对象(如真合并子)可通过非容器形式的构造器创建。

转发参数或返回值的实现

  没有约定需要转发的情形不使用显式的转发。

注释

  可转发参数转发返回值的函数可包含以下实现方式:

  • 使用特定的操作,以需被转发的表达式作为其操作数。
  • (仅对参数转发)使用标记字符 %参数绑定的变量。

  上述特定的操作可在被求值的表达式中构造显式的转发。

NPLA1 参照实现环境

  NPLA1 提供参照实现环境。其实现可在内部使用 NPLA1 库特性,提供给 NPLA1 用户程序

  NPLA1 参照实现环境和用户程序遵循部分不同的要求和约定。

  本章中的其它约定适用 NPLA1 参照实现环境,且可选地被用户程序使用。

NPLA1 初始求值环境

  NPLA1 以环境对象中的绑定作为公开的接口提供库特性,以进一步提供初始环境(initial environment) 作为求值环境,即用户程序初始的当前环境

  这些环境对象包含两类:

  这些环境应按语言规范要求的方式包含和展示所有可见的绑定

  除非另行指定,这些环境对象初始化后在用户程序访问前被冻结

  基础环境(ground environment) 是 NPLA1 程序可假定存在的一个根环境。

  基础环境是隐藏环境

  除非另行指定:

  • 基础环境不展示名称语言规范要求的除保留名称外的绑定。
  • 根环境是否展示基础环境中的绑定未指定。

  初始环境是一个包含基础环境作为直接或间接父环境的空环境

  实现可提供基础环境以外的根环境,允许派生实现定义在用户程序中修改其中的绑定的机制(而不一定是隐藏环境),直至被实现初始化参考环境的特定用户程序封装或冻结而避免进一步修改。

  如有必要,用户程序可通过派生实现定义的方式引入其它根环境。

原理

  这些环境对象设计为在参照实现环境提供,因为:

  • 不都保证能在用户程序中可移植地创建,而有必要在参照实现环境中提供。
  • 提供的库特性在可移植程序中可能经常出现,而适合在参照实现环境中提供。

  根环境在功能上不需要展示基础环境中的绑定,这允许简化初始化。但允许展示基础环境中的绑定也使派生实现能被简化。

注释

  提供基础环境和 Kernel 类似。

  互操作可能直接访问基础环境,这些操作应避免破坏实现和程序的假定。

  特性设计注记:

  • 为避免依赖逻辑上复杂的形式,一些特性当前在当前设计中排除。
    • 例如,依赖一阶算术的操作、其硬件加速形式的 ISA 表示的整数操作及依赖这些操作实现的数值塔(numerical tower) (en-US) 被整体忽略。
  • 上述忽略的特性可由派生实现补充,在派生根环境后按需进行 AOT(ahead-of-time) 优化(如 Kernel 的 $let-safe! 中包含的内容,其中引用基础环境的符号不再可变),然后组成类似基础环境。

  通过派生实现定义的方式一般依赖本机实现

NPLA1 实现环境初始化

  实现环境的初始化完成初始环境的准备,包括蕴含所有初始环境依赖的资源基础上下文(ground context) 的初始化。

  初始化基础上下文蕴含的根环境是求值默认使用的求值环境,初始化后可直接封装为基础环境使用。

  派生实现可在以上初始化结束之后,在运行用户程序之前完成其它初始化。

  初始化成功后,用户程序被运行;否则,程序非正常终止。

  以上初始化同时可能提供扩展字面量支持。

注释

  对初始化失败而终止的程序,建议但不要求实现给出诊断。

导入符号

  在环境中定义另一个环境中的同名变量,使被定义的变量是后者的引用值或值的副本,则称指定此变量名符号值在后者被导入(import) 前者。

注释 用户程序可导入环境中的符号值使用库中的绑定。

模块

  NPLA1 以绑定提供的语言特性被分组归类为模块

注释 同 [RnRK] 。

  模块的源(source) 提供特性的实现,可以是本机实现或者 NPLA1 程序。对应的模块分别是本机模块和源程序模块。

  模块的源可以是实现内建的,或位于实现环境提供的外部资源(如文件系统)。

  因为模块以绑定的集合的形式提供,需被包含在可访问的环境,或包含环境作为子对象的其它对象中。

  以环境对象作为模块的源的模块化方式称为环境作为模块(environment as module) 。[RnRK] 的 get-module 的结果和参照实现扩展环境的模块是这种方式的例子。

  模块可能包含子模块(submodule) 提供其特性子集。以环境作为模块时,环境子对象可作为子模块。

  从模块的源得到提供一个模块的所有绑定集合的环境对象的过程称为模块的加载(loading)

  模块加载可能失败。失败的模块加载引起错误

  根环境加载的失败不被直接依赖这些环境的 NPLA1 用户程序处理(而视为实现初始化的运行时错误)。

  一般地,模块和加载模块得到的环境对象没有直接对应关系:一个模块的绑定可以由一个或多个环境提供,一个已被加载的环境可能提供零个或多个程序可见的模块。但除非另行指定,一个模块的绑定不在超过一个的不相交的环境(之间没有直接或间接父环境关系)中提供。

  程序可通过加载外部模块来源取得模块。除非另行指定,这种模块以一个一等环境对象(可包含作为环境的直接或间接子对象)中的绑定提供。

标准库模块的初始化和加载

  标准库实现可作为语言实现实现环境初始化以提供模块时,可访问不作为公开接口提供的模块的源。

注释 派生实现可同时以标准库以外形式提供这些源为公开接口,用户程序也可显式地加载这些源对应的模块。

  除非另行指定:

  • 若这些源可能引起引入非公开的接口的副作用,则对应的模块不应被用户程序直接加载。
  • 假定加载这些模块时,当前环境是和标准环境或与其等价的其它环境。   * 注释 关于标准环境,参见 make-standard-environment
    • 其中,等价指使用其它环境不引入程序可观察行为差异。
    • 注释 等价的环境的例子包括以标准环境为父环境的空环境,以及这样的空环境导入符号的得到的结果。
    • 注释 若模块的加载不访问加载时初始的当前环境(通常仅在本机模块上适用),加载模块使用的环境可不影响可观察行为而不影响假定(即便和标准环境不等价)。

  违反以上要求或假定的程序行为未定义

原理

  以源程序模块实现时,一般不要求检查初始环境。这能有效减少实现的复杂性。

  因为标准环境不提供用户程序检查是否和其中定义的实体一致的直接的方法,通过替代的检查可能排除符合假定的初始环境。

注释

  这里的初始化可包含派生实现定义的其它初始化。

  虽然 NPLA1 标准库不作为接口保证提供这些源,这里的假定和 [ISO C++] [using.headers] 对引入标准库头的程序位置的限制类似:语言实现能有效地假定源程序中引入标准库头的上下文,因此标准库中的名称具有预期的含义。

模块稳定性

  提供模块绑定的环境依赖已知来源的绑定而确保稳定

  除非另行指定,模块中的特性依赖提供模块绑定的环境的生存期。

  除非另行指定,标准库实现应确保其中的模块在程序的生存期中可用。

原理

  特性依赖性允许实现操作的模块中绑定的合并子可具有静态环境是提供模块绑定的环境的子对象的合并子的实现。

注释

  稳定要求同 [RnRK] 的 get-module 的约定。但因为值稳定性和 Kernel 不同,NPLA1 的稳定绑定一般不可修改

  对标准库模块,稳定性要求一般表示其中的特性不能依赖用户程序运行时的非特定的当前环境,而可依赖从基础环境及从基础环境派生的新环境

  生存期可用的规则一般要求标准库实现在初始化后保存环境强引用

库接口约定

  基础环境的特性在根环境中直接绑定,统称根环境特性。

  关于特性的约束作用于接口描述。不改变可观察行为时,实现可使用不同的未指定的根环境提供绑定。

  描述模块接口的小节可以指定适用于该小节的模块约定。此时,描述的边界应能和其余的实体区分。

  接口可能提供关于宿主语言互操作的约定,作为对提供这些支持的实现的要求。

注释

  具体特性参见 NPLA1 根环境特性

  具体根环境的存在性未指定。在同一个环境中可见的不同变量可能来自不同的根环境。

  接口描述的顺序同 [RnRK] §4 的原理,允许接口仅依赖先前出现的接口派生实现。

  和 [RnRK] 不同,库主要提供一元谓词,也不需要为 <body> 隐含 $sequence 支持重新定义 $vau 等操作,不需要拆分 Kernel 的核心库特性到 [RnRK] §5 和 §6 。

库接口实体

  按实体区分,NPLA1 的库特性有两类:对象操作

  对象语言中可实现的操作以函数的形式提供,可以是本机实现宿主语言函数或由现有操作派生的合并子

  除此之外,派生实现可指定提供对象或操作对应的非常规函数

  操作的结果是对应的函数调用正常控制下取得的求值结果,即函数值;操作的作用即函数调用的作用

  根据操作的功能描述,对应的函数可能具有非正常的控制条件。此时,函数调用不取得函数值,操作不具有结果。

注释 非正常退出时,函数调用的求值结果可以是错误对象或派生实现定义的其它表示求值结果的实体。

  除非另行指定,函数调用时具有的错误条件是非正常的控制条件;其中,以异常实现错误条件的情形具有异常条件

  特定的操作约定对应的函数是终止函数全函数;这不适用于满足错误条件的情形。

  特定的操作约定对应的函数作为算法过程满足计算复杂度约定

注释 排除错误条件,指定复杂度的函数是终止函数。

  本章其余各节适用 NPLA1 对象语言中的这些操作。

  操作中的大部分具有特定的名称,满足函数名称约定

  其它操作不具有特定名称,可由上述操作间接地提供,如蕴含在某些操作涉及的函数值中。

注释

  在对象语言中不能直接表达的操作不能作为库特性,这些操作不对应库接口实体,其结果和作用仍照更一般的规则处理。

  渐进复杂度常以 O 记号指定上界。

  若函数调用总是取得值,指定复杂度的函数同时是全函数。

库特性实现分类

  库特性分为基本的(primitive)派生的(derived)

原理

  前者在设计上不分解为更小的其它特性的组合,通常需要本机实现;后者可由可移植的 NPLA1 源代码实现。

注释

  区分基本和派生的特性在设计上类似 [RnRK] 中的基本和库特性。

  注意和 [RnRK] 的库特性不同(而更接近宿主语言),NPLA1 的库特性是以 NPLA1 程序使用的接口而非实现的角度定义的,不总是使用对象语言实现,外延更广。

标准库

  本文档中要求的通过基础环境直接或间接提供的总称标准库(standard library)

  标准库的接口随语言规范在本章和参照实现扩展环境约定。

  核心库(core library) 是提供直接绑定在基础环境中的、保证可派生实现的接口的标准库模块。

  在参照实现环境中的不同标准库模块的绑定都可在基础环境访问。

  在参照实现扩展环境中的标准库模块以其它环境(通常作为基础环境的子对象提供)中的绑定和基础环境隔离。

  派生实现可以库的形式提供语言扩展或其它功能特性,扩充标准库。

注释

  因为库的定义和 [RnRK] 指定的不同,类似 [RnRK] §4 约定的基本特性,属于 NPLA1 库特性

扩展库

  基础环境也可提供的附带的其它接口,和标准库使用相同的约束。

  一些操作的描述使用等价的表达式求值指定。除非另行指定,这些表达式中:

  • 符号值和先前出现的函数同名,则指称对应的操作。
  • 默认使用基础环境作为求值环境。

常规函数约定

  本节提供作为库特性的函数的默认规则以简化库特性的描述。

注释 库的一般派生实现和用户程序的实现也建议参照本节约定。

  除非另行指定:

  • 操作以指定名称的变量的形式提供,求值为可参与函数合并的一等实体(但函数合并不一定保证是合式的可求值的表达式)。
  • 函数作为表达式,求值为合并子,其函数合并的求值蕴含函数调用
  • 本文档约定的函数在其调用不依赖用户程序提供的非终止函数时,总是终止函数
  • 本文档约定的函数蕴含以下情形时,调用非纯求值
  • 在指定对应的函数调用是纯求值的操作上,排除满足引起非纯求值的条件的作用的情形外的其余求值,仍应是纯求值。

原理

  一些操作因不保证排除副作用 ,对应的调用非纯求值。

  但通过补充约定,特定的作用可能视为未指定行为,且不被程序的其它行为依赖(如可变管理状态的改变),仍可假定其求值是视为纯求值。

注释

  无条件遍历访问循环引用子对象的程序具有 NPLA 未定义行为,在讨论终止函数时已被排除。

  对象的修改和改变可包括转移参数。

函数值约定

  除非另行指定:

实际参数约定

  除非另行指定:

错误处理

  除非另行指定:

  求值时引起的错误使求值中断,可引起副作用,这样的副作用总是后序于已被求值的表达式中引起的副作用。

  被错误处理和检查的函数不修改参数或者函数调用外创建的对象。

非常规函数

  续延是默认不符合常规函数约定的例外。

  非常规函数归类为对象而非操作,但调用时错误处理同常规函数。

注释

  类似 [RnRK] 而和 [RnRS] 不同,作为一等对象的续延和续延的实际参数是否求值无关,因此不是合并子,且默认求值算法不支持续延作为函数合并被求值;但续延可通过特定的操作转换为应用子。

函数名称约定

  除非另行指定,本文档以指定名称的函数表示具有名称的操作时,其命名由本节的规则约定。

  函数名(function name) 即函数的名称。

注释 除非派生实现另行指定,函数名是变量名

  为提供库的描述,本节同时约定这些函数名关联的函数具有的性质。

函数名称前缀

  确定为求值为操作子的函数名以 $ 作为命名的前缀。

原理

  同 [RnRK] ,操作子一般应在视觉上被强调而避免误用。$ 来自 Special form 。

函数名称后缀

  函数名的最后的字符表示函数预期满足特定的约束或具有特定的目的。

  以引用标记字符结尾的表示涉及引用的操作,参见以下约定。

谓词名称后缀

  NPLA1 中的谓词<predicate>,即返回类型为 <boolean> 的函数。

  谓词的名称使用 ? 结尾。

注释 这类似 [RnRS] 和 [RnRK] 。

  除非另行指定,以下引入的对象语言中的谓词对应的函数调用的求值是纯求值

  以下是谓词的典型实例:

  • 类型谓词:接受一个 <object> 参数,判断参数是特定的类型的对象。
    • 调用这些类型谓词不引起错误。
    • 仅当参数指定的对象具有对应类型时结果是 #t
    • 除非另行指定,这些类型谓词忽略值类别的差异。
  • 等价谓词:接受两个参数,判断参数是否属于同一个等价类。

  因为 <boolean><test>子类型,按照返回值为 <test> 的函数可在不严格要求 <boolean> 的上下文中起类似的作用,视为广义谓词(general predicate)

  谓词是广义谓词的子类型。

注释

  大多数上下文接受 <test> 而不严格要求 <boolean> 。这和 [RnRK] 不同

  大多数类型谓词判断的类型一般同文法约定,而无关值类别

  以具名的函数提供的类型谓词,其函数名称通常和文法指定的类型对应。

  引起错误可具有副作用,其求值不保证是纯求值。

  广义谓词不使用后缀 ? 。另见 [RnRK] §6.1.2 关于 $and? 的原理及 [R6RS-Rationale] §15.1 。

赋值函数名称后缀

  修改一个对象而不要求第一参数是引用值且不改变被赋值对象类型的赋值操作对应的函数名以 <- 结尾。

注释

  这通常和宿主语言的赋值操作对应,可能有附加的副作用而不是简单地替换值。

修改函数名称后缀

  除函数名以 <- 结尾外的操作中,为了蕴含(不直接通过求值操作数或其子表达式引起的副作用的)修改的函数是修改函数(modification function) ,其名称使用 ! 结尾。

  可变管理状态的改变不需要指示可修改;此外,类似地,不改变可观察行为隐藏状态的修改不属于上述修改。

注释 这类似 [RnRK] 。

  这类操作同时是改变操作

注释 这类似 [RnRS] 和 [RnRK] 。

  一些其它情形下,特定的函数不被视为改变操作但其调用仍可能引起副作用,这样的函数名不要求带有 ! 后缀:

  • 直接求值操作数、其子表达式关联的对象可能引起副作用。
    • 注释 这和 [RnRK] 类似。
  • 一些操作可能非确定性地包含使操作数指称或引用的对象的值有效但未指定的修改。

  修改函数的调用的求值可能因为不同的操作数的影响,而可能依赖具体的条件确定是否改变对象:

  • 当通过操作数直接指定的条件确定是否修改时。
  • 当通过操作数确定被修改的对象时。
  • 当通过操作数确定引起修改的对象时。

  这些函数的返回值未指定值

注释

  修改函数的调用不一定总是具有能视为修改的副作用,可能不改变任何对象。

  这些函数的返回值的规定和 [RnRS] 相同,但和使用可用谓词 inert? 判断的惰性(inert) 值的 [RnRK] 不同。

引用标记字符的函数名后缀

  一些操作以(可能的 ! 前)结尾引用标记字符和不以引用标记字符结尾的名称提供多个变体。其中不含结尾的引用标记字符的表示操作的结果不是引用值,要求按值传递

  其它一些操作可能只提供以 % 结尾的变体。

  不使用引用标记字符的函数及其函数值时,不因引入引用值违反内存安全

原理 这允许通过避免公开带有引用标记字符后缀的操作提供一个内存安全的子集,以在派生实现对语言进行裁剪。

  名称以引用标记字符结尾的操作属于以下分类之一:

  对可能在函数值中间接保留引用值的操作,以 % 结尾表示对应的函数返回时不要求返回非引用值

  其它可能在函数值中直接保留引用值的提供不同引用标记字符的多个变体的操作:

  • % 结尾表示函数使用不进行左值到右值转换折叠的引用值参数,或返回折叠的引用值。
  • & 结尾表示函数使用不进行左值到右值转换的折叠的引用值参数,或返回折叠的引用值。
  • @ 结尾表示函数使用不进行左值到右值转换的未折叠的引用值参数,或返回未折叠的引用值。

  以上引用值参数的使用指以依赖这些参数的方式构成函数的返回值和/或决定引起的相应的副作用;返回的引用值来自引用值参数(若存在)。

原理

  为满足适用性,同时考虑避免误用和允许使用引用避免复制,对一些操作显式使用以 %& 结尾的函数名称以得到特别关注。

  因为语义相关,结尾引用标记字符使用和绑定的引用标记字符相同的字符,但不复用具体规则。

  尽管设计时没有参照,使用函数结尾的引用标记字符和其它一些语言的类似特性的使用惯例也一致,如 PHP 的 function & 语法

注释

  按这些规则,函数名以 % 结尾的操作在取折叠的引用值时,可能同时实现被引用对象的转发。这相当于被访问的被引用对象作为宿主语言的 std::forward 的参数后的调用结果作为操作的结果,但此处一般仍然保证支持 PTC

  函数名以 & 结尾的操作取得的折叠的引用值可能是唯一引用

引用折叠的约定

  取得折叠的引用值的默认约定同NPLA1 默认规则

  以上函数返回值中:

  • 折叠的引用值对调用时引入的引用值(不论是否来自参数)有效。
  • 除非另行指定,不对同一个对象引用折叠多次。
  • 注释 这些规则不保证结果是完全折叠的引用值。返回折叠引用值的函数可因未完全折叠的引用值参数等返回未折叠的引用值。

  这允许函数的内部实现引入一次引用值时,对来自每个参数的引用值至多只需要实现一次折叠。

  推论:若参数都不是未折叠的引用值,调用名称不以 @ 结尾的函数不引入未折叠的引用值。

  若指定的操作按不同操作数可涉及或不涉及和当前不同环境下的求值,提供不保留引用值和保留引用值的多个变体的操作以便保证内存安全。

注释

  提供不保留引用值和保留引用值的多个变体的操作以便保证内存安全的操作包括可提供以引用标记字符结尾变体的函数不提供结尾引用标记字符对应变体的函数指定的操作。

引用标记字符函数名与内存安全的关系

  利用区分引用标记字符结尾的操作,可指定具体关于具体操作的对象语言接口的安全保证机制

原理

  函数名结尾的引用标记字符用于强调无法总是保证内存安全的危险操作

  一般地,仅在明确需要引用值时使用引用标记字符结尾的操作,而避免返回悬空引用。这类似宿主语言函数的 auto 而非 auto&& 的返回类型,但宿主语言中返回非引用类型的表达式两者含义不同。

  函数名不带有引用标记字符结尾的操作通过避免保留引用值提供一定的内存安全保证,而带有引用标记字符结尾的操作较容易引起注意。

  这符合易预测性

注释

  一个典型例子是在函数中返回标识符求值的表达式:

保留引用值的约定

  可能直接保留引用值的操作中,不带有引用标记字符的操作传递非引用值参数,其它函数转发参数

  可能直接保留引用值的操作包括容器构造器或访问器,以及可能使对象中包含引用值修改操作

  这些操作的结果或引起的副作用完全由实际参数(根据是否存在引用标记字符 % 指定是否不经过隐含的左值到右值转换)的值确定。

  其中,带有引用标记字符结尾的操作是直接保留引用值操作。

  容器构造器可在元素保留参数的引用值。作为结果的容器总是作为非引用值返回,即在结果中保留参数的引用值。

原理

  以上操作是否确定地保留引用值在一些情形容易证明附加调用安全,此时可放宽安全特性子集的条件确保安全性;在此不作要求。

  对构造器及部分修改操作区分引用标记字符结尾可强调一些非预期保留引用值的容易误用情形;尽管总是返回非引用值。

  因转发参数而被保留的引用值不会被返回值转换或类似的操作影响,在构造的容器对象作为非引用值返回时,仍会保留引用值。对应宿主语言中,可有更显著的差异,如构造器对应的 std::tuplestd::make_tuplestd::forward_as_tuple

可提供以引用标记字符结尾变体的操作

  部分操作使用以引用标记字符结尾的函数名

  可提供不同变体的操作被严格限制,以避免过度区分造成在使用上不必要的复杂性。

可能使结果包含引用值的容器构造器

  容器构造器作为包括典型的构造器,可提供不同的变体(或其中之一):

  • 不在函数值中保留引用值,实际参数发生左值到右值转换作为容器的元素,这减少误用悬空引用的可能性。
  • 在函数值中保留引用值,实际参数不发生左值到右值转换而直接作为容器的元素,是不安全操作,但可以确保构造的对象中包含参数指定的引用值。

注释

  当只提供没有结尾引用标记字符对应名称的操作时,不需要满足以下的约定。

  在结果中保留参数的引用值的容器构造器可能保留可能无效的间接值而属于不安全操作。

可能使结果包含引用值的容器元素访问器

  带有引用标记字符结尾的操作是直接保留引用值操作。

  函数名不带有标记字符结尾的访问器属于参数转发操作函数值转发操作

可能使对象中包含引用值的修改操作

  修改对象或对象的子对象无效化引用值而影响内存安全。

  对可能保留参数中的引用值的操作,内存安全也依赖这些操作的指定修改后的值的内存安全性。

  在判定内存安全的意义上,以下操作的所有参数都可能是被保留的间接值

  • 简单赋值(simple assignment)(包含于赋值操作)。
  • 列表元素改变器(mutator)

  修改的结果由实际参数(必要时经过隐含的左值到右值转换)的值确定。

  以上操作都要求检查表示被修改的参数是左值。

  以上操作中,带有引用标记字符结尾的操作在对象中直接保留引用值

可能间接保留引用值的操作

  一些操作可涉及不同的环境,参数在这些环境中被求值可能得到引用值。

  这些操作包括求值为操作子的以下函数:

  • 以求值 <body> 作为尾上下文的操作。
  • 以求值 <expressions> 初始化 <defindiend> 指定的对象的绑定构造
  • 以求值 <expression> 或视为 <expression><object>(及可能发生的返回值转换)作为唯一作用的函数。

  以上操作中,带有引用标记字符结尾的操作是间接保留引用值操作,表示求值结果不要求按值传递并可返回引用值

原理

  不提供函数值转发的形式,因为:

  和此处直接在参数中给出被求值表达式不同,应用子中的一些求值的操作不属于上述操作,而不提供结尾引用标记字符对应名称的操作

不提供结尾引用标记字符对应名称的操作

  其它操作不使用以引用标记字符结尾的函数名

  若这些操作的结果直接来自操作数或其子对象(和以 % 结尾操作的情形类似),则:

  否则,这些操作不具有引用值结果。

  部分操作的结果直接来自实际参数。此时,若不具有引用值结果,则蕴含左值到右值转换

原理 这些操作不会使用临时对象作为环境,所以不需要使用以引用标记字符结尾的变体要求注意区分返回引用值而避免误用。因此,不提供区分涉及引用的变体,这也使接口设计更清晰。

  这些操作包括以下小节的情形。

  部分操作涉及参数转发函数值转发。这些操作不包含可提供以引用标记字符结尾变体的操作中的个别变体

  其它不提供结尾引用标记字符对应名称的操作暂不保证支持保留引用值。

  部分操作的内存安全性和可提供以引用标记字符结尾变体的操作类似,也是在函数值中保留引用值的不安全操作,但仅在引用值参数被保留且以此访问被引用对象时体现。

  这包括直接保留引用值和间接保留引用值的不同情形。

  除可直接以引用值作为结果的操作和其它节的操作不相交,以下分类对操作的参数和函数值分别约定,可能相交。

可直接以引用值作为结果的操作

  一些求值为操作子提供的函数选取特定的参数进行求值,作为控制操作。

  操作数中被求值的参数直接决定是否为引用部分操作直接返回引用值。

  被求值的 <test> 蕴含左值到右值转换,其它被求值的参数不蕴含左值到右值转换,调用者需负责决定是否求值其它参数。

注释

  这类似宿主语言中参数传递和返回 auto&& 类型。

不以引用值作为结果的操作

  部分操作类似容器构造器保证返回非引用值,但并非直接以参数实现决定函数值:

  若非构造器的操作总是返回列表和其它对元素具有所有权的容器对象,返回的对象总是按值传递。

  为简化接口以及满足其它分类(如直接参数转发操作),不提供不保留引用值的操作。

  和提供不同的变体的作为构造器的操作不同,此处的情形的结果可能包含引用值(和以 % 结尾构造器的情形类似)。

  若需要排除通过参数引入的引用值,应进行适当处理使参数中不含有会使这些操作引入引用值的构造。

  类似保留引用值的容器构造器,这些操作可在结果中保留参数的引用值

直接参数转发操作

  部分不带有引用标记字符的参数转发操作可能直接保留引用值的操作,称为直接参数转发操作。

  函数名不使用引用标记字符,和可直接保留引用值的函数名使用引用标记字符不一致:

  • 本节约定的函数和可直接保留引用值的函数名中带有 % 结尾的函数同属参数转发操作,但后者同时有不带有引用标记字符的变体。
  • 本节不约定和可直接保留引用值的函数名中不带有引用标记字符结尾的函数对应的操作。

  这种不一致(和函数值转发操作不同)是预期的特性:

  和可直接保留引用值的操作不同:

  • 这些操作并非用于构造对参数具有所有权的对象,不适合提供不保留引用值的操作。
  • 这些操作并非用于取子对象,返回值不一定是引用值,和具体操作相关,不适合使用引用标记字符区分。
  • 为简化接口及满足其它分类(如不以引用值作为结果的操作),不适合提供不保留引用值的操作。

  本节约定的函数对引用标记字符的使用和可提供以引用标记字符结尾变体的操作的函数名的使用不一致,含义相当于前者的结尾的 %

  以下的函数值转发操作同时也是直接参数转发操作。

  其它函数的参数传递的一般规则参见引用值作为实际参数函数参数和函数值传递约定实际参数约定

函数值转发操作

  若其它情形确需非转发操作取得引用值,可使用带有 %& 结尾的操作及可直接以引用值作为结果的操作替代实现。

  本节约定的函数不使用引用标记字符,和容器元素访问器的函数名不使用引用标记字符一致:

  本节约定的函数和上述容器元素访问器的函数名中不带有引用标记字符结尾的函数同属函数值转发操作,但后者同时有带有引用标记字符的变体。

原理

  和容器构造器引入引用值的情形不同,不带有后缀 % 相对不容易引起误用,因为返回值保留的引用可以继续被返回值转换影响。

  例如,使用保证返回非引用值的涉及环境中求值的操作,引用值会在引用的对象生存期结束前被返回值转换而不影响内存安全。

可能间接保留引用值的无引用标记字符对应名称的操作

  类似可提供以引用标记字符结尾变体的对应操作,部分不带有引用标记字符的操作可能间接保留引用值

  这包括由类型为合并子的参数(而非 <body><expressions> )决定是否保留引用值同时对其它参数进行转发的操作。

函数名称中缀

  中缀 -> 在函数名中可能出现一次,表示其(移除前缀和后缀之后的函数名中的)左边为源类型名的值到右边为目标类型名的值的转换操作。

  除作为源类型的值外,可能支持可选的附加其它参数。

  除非另行指定,转换得到的值是纯右值

  除非另行指定,转换函数调用的求值是纯求值

  除非另行指定,若被转换的值的复制可能影响可观察行为,被转换的值被转发初始化返回值。其中的右值被转移时,使用对象的转移项的转移未指定。

  除非另行转定,按接口文法约定引入的操作数作为对应名称之间的转换,仅在引入其中之一的模块提供。具体规则如下:

  • 源或目标具有在同一个根环境或其中的环境引入的对象类型时,在根环境中提供转换函数的名称。
  • 源或目标具有从根环境中的 std.strings 以外的环境中引入的类型时,仅在引入其中之一提供转换函数的名称。
  • 否则,在模块 std.strings 提供转换函数的名称。

原理

  如有可能,被转换的值一般应避免被复制。在接口上要求转发右值避免不必要的复制。

  标准库模块 std.strings 支持 <string> 的值的有关操作。因为相关转换的潜在的普遍性,在此进行特殊约定。

注释

  一般地,转换操作是源类型的值作为单一实际参数的转换目标类型的构造器

不安全操作约定

  除非另行指定,执行时蕴含以下操作的操作是不安全操作

  按不安全操作引起不同的未定义行为和不同的间接值,以下小节对不安全操作进行分类。

  分类之间的关系详见保留间接值

  分类可能不完全的且可能相交的(不安全操作可能不属于任何一个分类或同时属于多个分类)。

在函数值中保留引用值的操作

  在函数值中保留引用值的操作包括按函数名称约定具有引用标记字符结尾的操作。

  直接保留引用值操作可配合带有返回值转换的操作,指定个别函数参数不再保留引用值。

  这些操作可引起之后的不安全引用值访问

  保留的引用值同时可能被构造循环引用

注释

  一些修改操作无效化引用值。这些引用值若被保留且被访问,可引起未定义行为。

  不引起被绑定对象无效的修改操作不被视为不安全操作,即便它们无效化子对象的引用值。

在函数值中保留环境引用的操作

  环境引用被返回时,总是被保留

  创建环境强引用的操作是在函数值中保留环境引用的操作。

  这些对象可能因为没有及时保存环境引用使环境对象和其中的绑定一并被销毁,而使引用值访问其中的对象的程序具有未定义行为。

  通过非引用的形式引入环境循环引用的操作同时可破坏环境的资源所有权。

注释

  直接返回有效的环境弱引用的操作不引起环境失效,不在此列。

在函数值中保留其它间接值的操作

  特定的支持强递归绑定而在函数值中保留其它间接值,可能存在其它无效的间接值

  在函数值中保留其它间接值的操作的强递归绑定过程中引用共享对象的中间值。

在环境中保留环境引用的操作

  环境中的被绑定对象可具有环境引用子对象,间接地在环境中保留环境引用。

  这些操作使当前环境或参数指定的环境(而不是合并子调用时创建的新环境)中的变量绑定包含间接值,后者可能依赖合并子调用时创建的新环境。

  被绑定的对象中可能保留环境引用,而使用环境间接地保留对象中的引用。

  使用这些操作时应总是注意被依赖的环境的可用性。

  若环境对象销毁,所有直接和间接依赖环境对象的间接值被无效化。这些间接值的不安全间接值访问引起未定义行为。

注释

  绑定的对象中可能保留环境引用的典型的例子是合并子对象的静态环境。

  创建合并子可在合并子中的环境中保留环境引用。

无效化被绑定对象或环境引用的操作

  特定的操作蕴含被绑定对象的存储期的结束而无效化它的引用值。

  若子对象的引用值已被绑定,这些引用值不需要通过其它不安全操作,而仅通过之后访问标识符求值结果即可引起未定义行为。

  因为环境稳定性要求,NPLA1 实现环境不提供这类绑定,因此这些操作不是不安全操作。

  但派生实现可能在语言实现中提供不满足环境稳定性的一等环境,其中对象的子对象的引用值被绑定为变量,且前者可能被修改。

  此时,这些操作可能允许无效化引用后的被引用对象被访问,成为不安全操作。

  类似地,无效化环境引用而无效化环境对象也可使其中包含的被绑定对象的引用无效化。

  但环境生存期要求,除非作为不满足环境稳定性的环境的被绑定对象,NPLA1 实现环境不提供唯一的环境强引用可被用户程序修改而使环境对象被销毁。

  在这个前提下,要通过使环境引用作为子对象被修改而结束环境对象的生存期,首先要求通过在函数值中保留环境引用的操作取得环境引用,得到包含环境引用作为子对象的对象,且保证只有这个对象保存环境强引用

  因此,若不存在其它不安全操作,即蕴含不存在在对象语言操作中无效化环境引用的情形。

  类似地,派生实现可提供不满足环境生存期中的销毁顺序的环境,而使用户无效化对应的环境对象。

  此时,这些操作可能允许无效化环境引用后的环境对象被访问,成为不安全操作。

副作用可能引入循环引用的操作

  一些操作不依赖其它不安全操作(保留引用值或环境引用)即可引入循环引用:

注释

  通过已有的不安全操作构造的引用值也可能引入循环引用(而引起未定义行为),但不是单一操作的副作用,不属于本节的实例。

  例如循环引用中使用 list% 的例子,$def!list% 不会因此被视为此处的不安全操作,因为单一操作的语义不引入循环引用值。

可能破坏环境稳定性的操作

  通过引用值进行的修改操作可因破坏环境稳定性而引起扩展 NPLA 未定义行为(不一定违反内存安全)。

  这包括以下可无效化对象包含的引用值而使可通过环境访问的某个子对象的同一性被改变,从而破坏环境稳定性的操作:

安全操作子集

  作为对象语言安全性保证的一部分,用户程序通过限制或避免依赖特定的不安全操作,在特定情形下可实现对象语言内存安全保证,而不需要分析具体操作的语义:

参照实现约定

  本节约定对 NPLA1 参照实现内部有效,不作用在用户程序。

  除非另行指定,NPLA1 参照实现环境作为公开接口提供的变量在根环境中绑定。

  除非另行指定,以变量绑定的提供接口没有严格的跨版本兼容性保证。

参照实现接口描述

  除非显式指定,空环境没有父环境

  约定的接口通过绑定在根环境中的名称提供,参见以下各节。

  描述操作的上下文中,结果指操作的结果而非求值结果

  除非是语义蕴含的操作结果或另行指定,所有取得函数值的操作满足:

  • 结果未指定值
    • 未指定值可以是 #inert或其它值,但满足忽略值时不引起可观察行为的改变。
      • 注释 这排除了引入 volatile 类型或非平凡析构的宿主值
    • 若这些操作在 [RnRK] 或 klisp 中存在结果是 #inert 的对应的操作,且未指定作为函数值的结果,则结果是等于 #inert 的右值。
  • 这些操作若存在对应的函数名,满足函数名称约定
  • 这些操作中隐式分配的和按接口约定转移所有权给操作实现的资源不存在资源泄漏

原理

  #inert 是单元类型的值。但是,未指定值并不依赖单元类型的性质,而只是需要一种可在对象语言中判断是否为实现(如 [RnRK] 的 inert? ),而避免使用特设的规则。

  取而代之,一些语言或运行时支持非一等的特设返回类型:void ,在对象语言中无法构造其值。这种特性引起一些实用上的困难而被考虑改进,替换成一等类型,如:

  即便被改进,这仍只是单元类型,而不够清晰地反映未指定的意图。

  未指定值蕴含的不确定性在对象语言中难以建模,因此直接通过语言规则约定直至作为内建的支持特性,能使语言的设计更简单

  未指定值的未指定性在接口意义上仍然不是一等实体,因为当前不提供如 inert? 这样的谓词。即便提供 inert? 也仅仅是判断值是否为 #inert ,而不是任意的未指定值。

  相对 inert? ,这类谓词可能是有用的,例如元语言可能需要判断未指定性质以简化其派生实现。但这类需求和实现细节相关,且当前缺乏实例显示在对象语言中无条件提供的必要性。更重要的是,若要求提供这种谓词,限制派生实现在未指定值的类型上维持开放类型设计。

  未指定值作为内建特性时,用户也无法直接提供这类谓词的派生(而不符合统一性)。在需要这类谓词时,派生语言设计可能视具体实现需要满足的条件补充。

操作符合性

  除非另行指定,以下关于操作实现的未指定行为

  • 运行时错误条件检查诊断的顺序。
  • 满足运行时错误条件时,按要求诊断之后的操作内部分配的资源的状态。
  • 变量绑定或其它可选地由派生实现定义的可调试实体的引入是否由本机实现的机制提供。
  • 是否存在不改变操作语义的附加的可被捕获的续延和这些续延的操作数。
  • 操作的实现使用的续延或者其它实体互操作和调试目的保留的名称。

原理

  以上未指定行为允许同一操作的接口约定和实现之间及不同实现之间允许存在引起调用操作时的可观察行为不同的次要差异。

实体实现约定

  在实现的意义上,库特性提供的实体的方式分为两类:

  • 调用本机 API ,提供本机实现
    • 直接调用本机 API 提供绑定和组合本机 API 实现功能的派生实现都是本机实现。
  • 通过组合既有对象语言提供的接口实现,即派生(derivation)

  因不预期通过派生实现,一些特性被设计为基本(primitive) 特性,总是通过直接调用本机 API 的本机实现提供。

  其它特性都是派生特性,可通过派生(derived) 实现提供:通过组合基本特性及其它已提供实现的派生特性实现。

  典型地,派生实现通常不依赖实现特定的互操作接口的本机实现,这类派生实现是非本机的(non-native) ;其它的派生是本机的。

  以下的派生操作应能以派生的方式提供。派生操作是否以派生的形式提供未指定。

注释

  派生特性的实现方式可类似 [RnRK] 。

  非本机的派生实现通常以求值特定的对象语言源代码引入。

  非本机的实现最终依赖本机实现。

预定义对象

  除操作外,实现可定义特定名称的对象以变量绑定的形式在库中初始化,直接提供具有预设目的的可编程特性。

NPLA1 根环境特性

  本章指定在根环境提供的 NPLA1 标准库特性,即根环境特性

  根环境基本特性是单独的模块。

  基础派生特性起的各节提供其余要求 NPLA1 实现直接支持的各个模块的操作。

原理

  一些特性参照和类推扩展 [RnRK] 。

  同 [RnRK] 的设计,eval$vau是体现对象语言设计的光滑性的主要原语。

  因为保留引用值的不安全操作的支持,类推 eval%$vau%

  在本设计中,后者在逻辑意义上更基本,即便不指定为派生

注释

  一些特性可约定处理的值的宿主类型

不安全函数索引

  本节按不安全操作约定的分类对提供根环境中的不安全操作的函数进行归类。

  在函数值中保留引用值的不安全操作已被命名归纳和函数分类枚举,此处从略。

  不安全操作中,在参数以外直接引入间接值的操作仅有以下的在函数值中保留引用值的不安全操作:

  附加调用安全包括在函数值中保留引用值的不安全操作的调用。

  当前,这种操作包括 assign!

  隐藏环境排除可修改对象的引用,通过冻结环境保证而提供静态的证明。

在函数值中保留环境引用的函数

  在函数值中保留环境引用的操作包括:

  • 基本操作
    • make-environment
    • copy-environment
    • lock-environment
  • 派生操作
    • lock-current-environment
    • derive-current-environment
    • make-standard-environment
    • derive-environment
    • $provide/let!
    • $provide!

在函数值中保留其它间接值的函数

  在函数值中保留其它间接值的操作包括:

在环境中保留环境引用的函数

  在环境中保留环境引用的操作包括派生操作

  • $provide/let!
  • $provide!

无效化被绑定对象的函数

  当前不提供无效化被绑定对象的操作

  这可包含直接移除变量绑定的操作。

副作用可能引入循环引用的函数

  副作用可能引入循环引用的操作包括可能自赋值而引入循环引用值的操作:

  • assign@!
  • assign%!

可能破坏环境稳定性的函数

  可能破坏环境稳定性的操作包括下列两类:

函数分类

  本节对函数按名称和其它不同性质进行分类。

  在 NPLA1 参照实现环境提供的函数具体详见根环境基本特性和基础派生特性。

  除非另行指定,本节约定的函数属于 NPLA1 参照实现环境。

  本节约定的函数提供的部分操作属于转发

注释

  转发参数或返回值的实现中可使用 forward!

可提供以引用标记字符结尾变体的函数

  本节中的可提供以引用标记字符结尾变体的操作的以下分类不相交,但部分分类中函数名不带有引用标记字符结尾的操作可能和不提供结尾引用标记字符对应变体的函数中的操作相交。

  除非另行指定,符合以下分类的操作:

注释

  函数值约定已指定函数值默认不保留引用值;这没有涵盖 <body> 的求值结果。

可能使结果包含引用值的容器构造器函数

  可能使结果包含引用值的容器构造器包括:

可能使结果包含引用值的容器元素访问器函数

  可能使结果包含引用值的容器元素访问器包括基本派生特性中的以下操作:

  • first
  • first@
  • first%
  • first&
  • rest%
  • rest&
  • restv

  注意 restvrest% 总是构造列表,并不直接返回子对象的引用(另见引用值构造函数);其它访问器若带有引用标记字符,可直接返回引用值。

  此外,标准库中的函数值转发操作中部分函数也符合容器元素访问器的要求,但当前不提供带有后缀标记字符的变体。这些函数包括:

可能使对象中包含引用值的修改函数

  可能使对象中包含引用值的修改操作包括:

可能间接保留引用值的函数

  可能间接保留引用值的操作包括以求值 <body> 作为尾上下文的操作:

  • 结果是合并子或用于在环境中绑定合并子的构造器操作。
  • 核心库函数中的绑定操作。
  • 在尾上下文中求值 <expression> 参数视为的 <object> 的函数,包括 eval@evaleval%

  参见环境基本函数核心库

注释

  以上操作中的求值符合词法闭包规则。

不提供结尾引用标记字符对应变体的函数

  本节列举不提供结尾引用标记字符对应名称的操作

注释

  派生实现中通常使用 forward!进行转发实现上述保证。

可直接以引用值作为结果的函数

  可直接以引用值作为结果的操作包括:

不以引用值作为结果的函数

  不以引用值作为结果的操作的返回值总是按值传递的操作涉及的容器包括列表、其它的封装类型对象,包括:

直接参数转发函数

  直接参数转发操作包括:

  核心库函数中的绑定操作的非 <environment> 的形式参数支持转发。

  参数形式上被转发但操作的语义并非总是转发到其它操作的操作不使用本节的名称约定,如以下仅有第二参数支持转发的操作是提供结尾引用标记字符对应名称的函数,有对象基本函数中的:

  • assign%!
  • assign@!

注释

  map1 同时是不以引用值作为结果的操作

函数值转发函数

  NPLA1 参照实现环境的函数值转发操作包括以下访问对象或被引用对象自身或子对象的函数:

  基本操作的不具有名称的相关操作中参数和函数值原生支持的转发操作包括:

  使用 make-encapsulation-type返回的访问器合并子。

可能间接保留引用值的函数

  可能间接保留引用值的操作包括:

引用折叠相关函数

  本节列举引用折叠相关操作。

  只有函数名以 @ 结尾的函数可能引入未折叠的引用值,包括:

  此外,在参数保留引用值的修改操作可能使现有的引用值成为未折叠的引用值,包括:

  函数 uncollapsed?区分未折叠的引用值。

  蕴含左值到右值转换的函数可以消除引用值,如:

  其中,collapse 依赖 uncollapsed? 而针对未折叠的引用值消除引用值,实现引用折叠。

  其它一些函数可能在非本机实现中依赖未折叠引用但不在接口中体现,如 rulist

引用值构造函数

  当前构造子对象引用的操作有:

非引用值构造器函数

  一些函数构造非引用值,包括:

  • 核心库转换函数:
    • $bindings/p->environment
    • $bindings->environment
    • symbols->imports
  • 续延库转换函数:
    • continuation->applicative

  其中,明确转移而不是复制被转换的右值的函数有:

  • continuation->applicative

根环境基本特性

  NPLA1 通过预定义对象的形式提供可选的模块

  根环境基本特性是除了这些模块的以变量绑定形式提供的不要求可派生实现的特性。

  根环境基本特性的被绑定对象包括基础环境提供的预定义对象和在基础上下文的根环境中初始化的基础基本操作(grounded primitive operations) 的实现。

  派生实现可以通过提供不公开不安全操作的根环境,但不符合此处的规格要求

  若派生实现不提供在函数值中保留其它间接值的操作,可以简化部分对象基本函数中与之关联的操作的实现。

  当前在根环境中的直接提供绑定的特性不依赖 <number>

原理

  和 [RnRK] 不同,为简化设计,不提供可选(optional) 的合并子。

  可选功能不应被必要功能依赖。

  根环境的基本特性为组合其它实用特性提供,而数值在设计中不是必要的功能特性。

  部分其它原理参见根环境对象定义。关于引用值的处理另见函数分类

注释

  部分可选的 Kernel 合并子被直接提供。

  和 [RnRK] 不同,一些函数显式地操作引用值,包括未折叠的引用值

  和 [RnRK] 不同,求值算法不直接处理对象的引用值。

  为简化实现,部分提供 % 等后缀的函数不被派生。

  因为设计原因,不提供以下 Kernel 合并子对应的操作:

  • copy-es-immutable
  • inert?
  • ignore?

  考虑(可变对象的)一等引用和绑定构造绑定引用的的平摊复杂度,不提供需要同时转换不同层次子项的 copy-es-immutable 操作。

  其它没有包含在以下节中的 Kernel 合并子对应的操作可能会在之后的版本中被支持。

根环境对象定义

  除为提供根环境特性的模块以外,当前根环境不定义对象。

  以下各节引入的变量都表示操作。

等价谓词基本函数

模块约定:

  本节的操作不修改参数对象。

  本节的操作的结果是 <boolean> 类型的纯右值。

  用户定义的类型提供的等价谓词应满足和 NPLA1 提供的等价谓词的语义一致的等价关系,否则若谓词被求值,行为未指定。

  一些具有项节点作为表示的对象的子对象具有递归相等性,仅当子对象符合以下递归相等关系:

  • 对不表示有序对的节点,同 eqv?
  • 否则,同每个元素对应 eqv? 对应满足 eqv?

  判断子对象递归相等性的对象相等时,其续延未指定。

  若右值之间 eqv? 比较结果是 #teq? 比较结果未指定。

操作:

eq? <object1> <object2>

  判断参数同一

  当且仅当两个参数是指定同一对象时,比较结果是 #t

  eq? 的复杂度是 O(1)

eql? <object1> <object2>

  判断表示参数的项的值数据成员相等。

  忽略表示参数的项的值数据成员以外的子对象:若参数是列表,则视为空列表;若参数是有序对,则视为仅具有最后一个元素。

  若参数是引用值,则被比较的项是表示它的被引用对象的项。

  当且仅当被比较的项的值数据成员相等时,比较结果是 #t

  值数据成员相等蕴含参数的动态类型相同

eqr? <object1> <object2>

  判断表示参数的项的数据成员同一。

  当且仅当表示被比较的项的值数据成员指定宿主语言中的同一对象(即引用相等)时,比较结果是 #t

eqv? <object1> <object2>

  判断非枝节点表示的值相等。

  若参数是引用值,则被比较的值是它的被引用对象。

  根据项的内部表示:

  • 当表示值的项都是枝节点时,同 eq?
  • 否则,若这两个参数的类型不同,则结果是 #f
  • 否则,若这两个参数的 eql? 比较结果是 #t ,则结果是 #t

  若两个参数的 eqv? 比较结果是 #f ,则这两个参数以 eq? 比较结果总是 #f

  除非互操作(参见以下描述)或派生实现另行指定,不等价的函数的 eqv? 比较结果是 #f

  除以上规则确定的结果外,eqv? 对合并子或列表的比较结果未指定。

  在互操作的意义上,当前 eqv? 定义的合并子的相等性由宿主类型== 或不影响可观察行为的其它宿主环境提供的 == 操作通过和 eql? 比较相同的方式确定。

  除非另行指定,具有本文档引入的类型且不涉及互操作意义上用户自定义值的比较的操作数使用以上 eq? 以外的谓词比较的求值应保证能终止。

原理

  除任何其它类型都可作为 <object> 的子类型开放类型映射类型系统通常要求避免依赖 <object> 上的其它的良序和良基的理论,以避免对现有类型系统的扩展时需要修改已有的类型的相关操作。

  不需要依赖序的等价谓词可为名义类型提供直接的支持。

  NPLA1 提供默认相等为抽象相等,对任意的值适用。

  NPLA1 还提供对一等对象保证结果有意义的引用相等操作。非一等实体的引用相等关系未指定。

  当前 NPLA1 不支持 [EGAL] ,因为 [EGAL] 要求存在分辨任意对象的值是否可被修改的元数据。

  因为对应等价的不变性关系不具有唯一性,且可能允许不唯一的方式引起副作用(如缓存),和 [RnRK] 不同,不以基本操作提供 equal? 对任意对象提供一般的相等操作。

  未指定 eq? 的比较结果可允许实现复用存储右值的驻留对象。

  eql? 实际比较宿主值的相等。允许 eqv?eql? 的不同可允许一对多的类型映射下比较对象语言的值的相等。(而多对一的类型映射 eql?eqv? 可一致地比较。)

  但是,当前实现中,大多数一对多映射的类型(如环境)都没有引起使 eql?eqv? 不同的比较实现,因为不同宿主值类型的对象具有足够显著的差异,在大多数上下文不通过一些具有不可忽略开销的转换机制(如锁定环境弱引用转换为环境强引用),无法直接相互替换而保证行为差异可被忽略,因此逻辑上不适合定义为相等的。

  而基于性能等理由,等其它一对多映射的类型(特别是可能基于宿主类型的值的子集的,如 NPLA 数值类型中的 <integer> )的值的比较也没有特别的处理,而引起 eqv?eql? 的不同。

  这些类型可能需要其它针对特定类型的等价谓词(如 =?)进行相等性的比较。

  类似 [RnRS] ,不同类型决定 eqv? 的结果是 #f ,但此处类型相同的含义不通过类型分区定义。

  类似 [RnRS] ,行为不等价的函数的 eqv? 结果原则上应为 #f ,但这种等价性一般不可证明而无法保证,特别在关于语言实现以外的调用上。

  为支持互操作使用本机实现及避免限制合并子的子类型的开放性,允许这些实现另行指定规则,假定引起程序可观察行为差异的函数调用调用名义等价。

注释

  通常,等价谓词比较的求值应保证能终止且对非列表项和 n 个子项的列表分别具有 O(1)O(n) 平摊复杂度。这是依赖数据结构实现的细节;语言不需要约束这个性质。

控制基本函数

$if <test> <consequent> <alternative>

  条件分支,按条件成立与否返回 <consequent><alternative> 之一,可能是引用值。

$if <test> <consequent>

  省略第三操作数的条件分支,条件成立时返回 <consequent>

  和 [RnRK] 不同而和 [RnRS] 类似,如 <test> 的求值结果非 #f 即选择 <consequent> ,且支持省略第三参数。

  若省略 <alternative><test> 求值为 #f ,则结果未指定

注释

  对 <test> 的处理的主要原理和 Kernel 的 $and? 不要求尾上下文的表达式求值检查类型一致。

  若需要检查类型避免误用,可以派生提供其它函数;相反的派生无法利用更简单的实现。

原理

  和 [R7RS] 类似,但和 [Racket] 及 [RnRK] 不同,省略 <alternative> 被支持。

  和 [R7RK] 不同,不使用 #inert ,参见关于参照实现接口描述的原理。

  和 [RnRK] 中的相关讨论结论不同,是否省略 <alternative> 的形式应是一致的。这是因为:

  • NPLA1 不假设作为基本控制操作的 $if 的作用(仅要 <consequent><alternative> 求值的结果,或仅为了副作用)。
  • $if 不假设用户对 <consequent><alternative> 顺序选择性偏好,以避免限制用户选择否定谓词简化 <test> ,从而支持变化的自由

  此外,NPLA1 使用显式的 <expression-sequence>(而不是 <consequent><alternative> )语法表示顺序求值,这不适合基本的控制操作子:

  若分离二操作数和三操作数其它形式,则二操作数可以使用 <expression-sequence> ,即 $when

  但依赖 <expression-sequence>$when 不应是比具有 <consequent> 的二操作数形式更基本的操作。

  因此,仍然需要有 $when 以外的省略第三参数的基本控制操作子。基于统一性,对应函数名仍然为 $if 。(尽管使用了相同的原则,这和 Kernel 的结论恰好相反。)

  与此类似,和 [Racket] 的理由不同,不因为 $when 提供只强调副作用的操作而取消 $if<alternative>

  NPLA1 不会如 [Racket] 一样在此避免遗漏 <alternative> 导致的非预期结果。这并不违反适用性,因为不使用 <alternative> 的结果非常显然,同时选择使用 $if 这样的基本控制操作而不是更特定派生控制操作或更高级的抽象已蕴含注意误用的必要性。

  一般地,NPLA1 不提供强调只存在副作用的操作。返回未指定(而不要求被使用)的求值结果的情形并不表示只有副作用,因为副作用是否存在原则上依赖具体操作。这和 Kernel 的 #inert 以及 [Racket] 的 #<void> 值即便在实现上都一致,但含义不同。

  另见 $when 的说明。

对象基本函数

模块约定:

  因为真列表的限制,列表左值只能引用完整的列表的对象,而不支持部分列表。

  这影响 set-rest!set-rest%! 的第一个参数。

操作:

null? <object>

  判断操作数是否为空列表。

nullv? <object>

  判断操作数是否为空列表纯右值。

  同 null? ,但不支持引用值。

branch? <object>

  判断操作数是否具有枝节点表示。

branchv? <object>

  判断操作数是否为具有枝节点表示的纯右值。

  同 branch? ,但不支持引用值。

pair? <object>

  <pair>类型谓词

pairv? <object>

  判断操作数是否为有序对纯右值。

  同 pair? ,但不支持引用值。

symbol? <object>

  <symbol> 的类型谓词。

reference? <object>

  判断操作数是否为引用值。

unique? <object>

  判断操作数是否为唯一引用

modifiable? <object>

  判断操作数是否为可修改对象或可修改对象的引用值。

temporary? <object>

  判断操作数是否为临时对象或临时对象的引用值。

bound-lvalue? <object>

  判断操作数是否为被引用的被绑定对象左值。

  绑定临时对象的引用类型的参数不被视为左值引用。

  配合[$resolve-identifier% 引用标记绑定的变量,可确定实际参数是否为左值;参见 $lvalue-identifier?](#基本派生特性) 。

  使用 bound-lvalue?& 引用标记字符绑定的变量,可确定实际参数是否为引用。

uncollapsed? <object>

  判断操作数是否为未折叠的引用值。

deshare <object>

  取指定对象取消共享的值。

  同 idv ,但显式转换操作数中具有共享持有者的值数据成员为不共享的值,且不转移宿主值

原理

  因为提供在函数值中保留其它间接值的操作,这个区别是必要的。否则,使用 idv 替代应不影响可观察行为

as-const <object>

  取指定对象的不可修改的引用。

  同 id ,但当参数是引用值时,结果是和参数引用相同对象的不可修改的引用值。

expire <object>

  取指定对象的消亡值

  同 id ,但当参数是引用值时,结果是和参数引用相同对象的唯一引用。

  可用于显式地指定之后被转移的对象,而不需要直接转移参数。

  特别地,指定列表的引用值被转移时,不需要立即转移列表的每个元素,而允许之后通过绑定构造等方式选择转移的子对象

  可能包含立即转移的操作如 forward!

原理

  这不直接转移对象,而不是修改操作函数名不以 ! 结尾

注释

  这个函数类似宿主语言标准库中作用在对象类型实际参数的 std::move ,可能减少没有经过复制消除复制或转移而改变使用这个函数的结果对象的副作用。

move! <object>

  转移对象。

  若参数是不可修改的左值,则以复制代替转移;否则,直接转移表示参数对象的项

  结果是不经返回值转换的项。

注释 另见转移的注意事项

transfer! <object>

  转移对象。

  同 move! ,但使用对象的转移,而不是项的转移,避免宿主对象转移消除而允许调用宿主对象的转移构造函数

注释 参数被转移后,和返回值转换引入实质化临时对象时可能具有的转移的效果(仅在互操作时可见)可能相同。

ref& <object>

  取引用。

  对引用值同 id ;对具有共享持有者的值数据成员的对象也视为左值。通过后者构造的引用值不被检查。

  取得的引用值是不安全引用值

原理

  因为提供在函数值中保留其它间接值的操作,对共享持有者的值数据成员的对象使用不同的处理。否则,对引用值参数的情形,使用 id 替代应不影响可观察行为。

assign@! <reference> <object>

  赋值<reference>被引用对象为指定对象的值,且 <object>蕴含左值到右值转换不被折叠

  赋值被引用对象前首先检查 <reference> 是可修改的左值。

  赋值对象直接修改被引用的对象,但不无效化参数指定的引用。

  支持修改 <reference> 指定的子对象引用的被引用对象。

注释 被赋值替换的子对象的引用可被无效化。Scheme 的 set![SRFI-17] 提供具有类似作用的支持,但第一操作数限于 set! 且为特定的过程调用;Kernel 没有类似的操作。另见赋值的注意事项

有序对基本函数

cons <object1> <object2>

  构造参数指定的两个元素构成的有序对。

  结果是 <pair> 类型的值。

注释

  不保留 <object2> 的引用值,但这不涉及 <object2> 是有序对或有序对的引用值时其中可能具有的元素。

  若 <object2> 中存在元素,直接被作为结果的元素,不经过返回值转换。

cons% <object1> <object2>

  构造参数指定的两个元素构成的有序对,保留引用值。

  同 cons ,但参数是引用值时,直接以其值作为元素的值,而不以其被引用对象的值创建有序对。

注释 这允许被构造的结果中存在和参数相等的引用值,而非其被引用对象的副本。

set-rest! <pair> <object>

  修改列表的第一个以外的元素。

注释 和 [RnRK] 的 set-cdr! 类似,但检查列表是左值,且不保留被添加元素中的引用值。

set-rest%! <pair> <object>

  同 set-rest! ,但保留引用值。

注释 和 [RnRK] 的 set-cdr! 类似,但检查列表是左值。

注释

  和 [RnRK] 不同,NPL 不支持列表中存在环。

  不使用相同的对象左值的 cons% 调用或修改操作导致循环引用,用户应自行避免未定义行为

  结果具有的属性不被影响。

符号基本函数

desigil <symbol>

  移除符号中的引用标记字符 &%

  判断符号非空且以 &% 起始,结果是移除起始字符的参数。否则,结果是参数。

  不处理引用标记字符 @

环境基本函数

模块约定:

  为避免引入过于容易引入循环引用,仅通过个别操作引入环境强引用

  • make-environment
  • lock-environment

操作:

eval@ <object> <environment>

  在参数指定的环境中求值,结果作为函数值。

  <object> 在求值前被视为 <expression>

  <object>蕴含左值到右值转换

eval-string% <string> <environment>

  在参数指定的环境中求值作为外部表示的字符串。

注释

  类似 klisp 的同名操作,但保留引用值。

  不提供类似 eval@eval-string@ ,因为不论参数的值类别,求值总是依赖的参数字符串的值。

eval-unit <string>

  规约字符串表示的翻译单元以求值。

  直接使用当前环境,但其中求值不在尾上下文,也不改变当前续延。

注释

  和 eval-string% 类似,但不支持指定环境,求值不在尾上下文,也不改变当前续延。

  使用的实现环境可以是 REPL 环境。

bound? <string>

  判断指定字符串对应的符号是否被绑定。

$resolve-identifier <symbol>

  解析当前环境中的标识符。

  结果是解析结果中的项。

注释 参数不按成员访问规则确定值类别,也不按解析名称表达式的规则确保结果总是左值,可保留消亡值

$move-resolved! <symbol>

  转移解析标识符的对象。

  和 $resolve-identifier 类似,但直接取被绑定对象并尝试从环境中转移。

  若环境被冻结,则复制被绑定对象;否则,直接转移对象的项

  一般应仅用于被绑定的对象不需要再被使用时。

() copy-environment

  递归复制当前环境。当前忽略特定的父环境

  结果是新创建的环境的强引用。

警告 这个函数仅用于测试时示例构造环境,通常不应被用户程序使用,且可能在未来移除。未确定环境宿主值时可引起未定义行为。

freeze-environment! <environment>

  冻结环境。

  这个操作处理操作数指定的一等环境。

注释隐藏环境初始化时的相同操作参见冻结操作

lock-environment <environment>

  锁定环境:使用环境弱引用创建环境强引用

  检查参数是环境弱引用,若失败则引起类型错误。结果是对应的环境强引用。

  强引用可能引起环境之间的不被检查的循环引用,用户应自行避免未定义行为

make-environment <environment>...

  创建以参数为父环境的环境。

  和 [RnRK] 不同,除对象类型外,没有对列表和绑定的附加检查。

  结果是新创建的环境,是环境强引用,具有宿主值类型 shared_ptr<Environment>

weaken-environment <environment>

  使用环境强引用创建环境弱引用。

  检查参数是环境强引用,若失败则引起类型错误。结果是对应的环境弱引用。

原理 因为 NPLA1 需要精确控制所有权而不依赖 GC,这可用于派生实现某些操作(如 $sequence 必要的)。

$def! <definiend> <expressions>

  定义:修改当前环境中的绑定。满足绑定构造的约定。

  $def!$defrec! 在求值 <expressions> 后,进行类型检查,确保环境没有被冻结后添加绑定。

  对 <definiend> 中已存在的标识符的绑定,保证直接替换对象的值,对象的引用不失效

注释

  类似 [RnRK] ,对在 [<body>] 中某些未被直接求值的子表达式(如 $lambda<body>),因为其中的求值依赖 $def! 表达式求值后的环境,在之后仍可以实现递归。

  类似 [RnRK] 的 $define! ,但绑定构造的约定存在不同规则。

  求值 <body> 后进行类型检查和 [RnRK] 的 $define! 不同。

  由于递归调用依赖环境中的绑定,修改以上定义引入的绑定后可影响被递归函数的调用。

$defrec! <definiend> <expressions>

  递归定义:修改绑定,同 $def! ,但在绑定时针对 <definiend> 指定的操作数树中的绑定名称有附加的处理以支持直接递归。

  除和 $def! 相同过程的常规绑定(求值 <expressions> 和绑定符号)外,支持强递归绑定,其操作数树的附加处理分为两阶段;每个阶段深度优先遍历 <definiend> 指定的操作数树,对每个符号进行附加处理:

  • 在常规绑定前,每个遍历的待绑定符号在目标环境(被定义影响的环境)中预先进行绑定,保证指称一个对默认对象的弱引用,其中默认对象具有调用总是抛出异常的非真合并子的值;和这个弱引用的共享的强引用被临时另行保存。
  • 在常规绑定后,再次遍历操作数树,对每个实现支持的合并子的的值,替换之前在环境中保存的共享定义为默认对象的共享强引用,最后释放先前临时保存的默认对象的强引用。

  调用默认对象时:

  常规绑定后转移未被 <expressions> 求值影响的绑定中的默认对象的所有权到环境中,但不影响绑定目标在对象语言中指称的值。

  在环境中未被 <expressions> 求值替换的绑定,在 $defrec! 求值仍指称默认对象(而不会是持有真合并子的值),若被作为合并子调用,则显示存在循环递归调用。

  和 $def! 不同,求值 $defrec!<expressions> 前保证 <defindiend> 中的名称已存在默认定义,求值 <expressions> 可访问对应的名称而不因名称解析失败而引起错误

注释

  和 $def! 不同,即使不计绑定修改环境的副作用,常规绑定后的操作使 <expressions> 不在尾上下文求值。

  这允许递归定义的名称在绑定完成前指称对象。例如,派生标准环境,当前环境中未绑定变量 ab 时:

  • 求值表达式 $def! (a b) list b ($lambda () 1) 因为被求值的 b 未被绑定而引起错误。
  • 求值表达式 $defrec! (a b) list b ($lambda () 1) 不需要 ab 已被绑定(即便 b 并不在 $lambda<body> 中),求值后 a 为默认对象。
  • 求值表达式 $defrec! (b &a) list ($lambda () 1) b 绑定要求同上,但求值后 a 可能为默认对象(操作数树中的同级叶节点被未指定的绑定顺序影响)。

  这也允许在 $vau/e等表达式的 <environment> 指定的静态环境使 <body> 不能访问目标环境时,直接定义递归函数。

  递归定义的对象中的值数据成员可能具有共享的持有者。若为合并子,直接调用会利用替换的值重新访问所在的环境。复制和转移这样的值不会改变被访问的环境。若访问的环境失效,则抛出异常,或无限递归调用自身。

  特定情形使用 deshare可去除共享和避免以上可能非预期的行为。

  另见]环境](#npla1-环境)。

合并子基本函数

  和 [RnRS] 及 [RnRK] 不同,<body> 可以是多个项,而不再派生另外的变体支持顺序求值。

  引入合并子的操作子不求值 <body> ,后者在被调用时替换操作数以后被求值。这允许安全地使用 $def! 而不需要 $defrec! 进行递归绑定

  检查失败的错误是(可能依赖类型错误的)语法错误

$vau/e <parent> <formals> <eformal> <body>

  创建指定静态环境的 vau 抽象

  创建的对象是操作子

$vau/e% <parent> <formals> <eformal> <body>

  同 $vau/e ,但保留引用值。

wrap <combiner>

  包装合并子为应用子

  包装应用子可能符合包装数溢出的错误条件

wrap% <combiner>

  同 wrap ,但参数不蕴含左值到右值转换,在结果中保留引用值

unwrap <applicative>

  解包装应用子为底层合并子

  左值参数解包装的结果是合并子的子对象引用

原理

  指定 <parent> 作为静态环境可通过被绑定实体的所有权控制一等对象的生存期。同时,在没有 safe-for-space 保证时,仍可有效避免资源泄漏

注释

  和 [RnRK] 不同,因为支持保存环境的所有权,提供 $vau/e 作为比 $vau 更基本的操作。

  不考虑所有权时,eval$vau 可派生 $vau/e

  和 [RnRK] 不同,参数是右值时解包装的子对象被复制。由这些合并子创建的操作子当前仍不足以取代内置的一等操作子,因为不支持只能转移而不能复制的对象。传递这些对象作为操作数会引起构造失败的异常。

错误处理和检查基本函数

  以下函数提供错误处理的相关支持。

raise-error <string>

  引发表示错误的异常

raise-invalid-syntax-error <string>

  引发包含参数指定的字符串内容的语法错误

raise-type-error <string>

  引发包含参数指定的字符串内容的类型错误

check-list-reference <object>

  检查对象是否是列表引用:若检查通过转发参数作为结果,否则引发错误对象。

check-pair-reference <object>

  检查对象是否是有序对引用:若检查通过转发参数作为结果,否则引发错误对象。

封装基本函数

() make-encapsulation-type

  创建封装类型。

  和 [RnRK] 类似,结果是三个合并子组成的列表,其元素分别表示用于构造封装类型对象的封装(encapsulate) 构造器、判断封装类型的谓词和用于解封装(decapsulate) 的访问器:

  创建的封装类型支持判断相等(参见 eqv?),相等定义为被封装的对象的子对象递归相等性

注释

  和 [RnRK] 不同,使用构造器初始化封装的对象作为容器,具有作为其子对象的被封装的对象的所有权。

  需要注意保存被构造的封装对象。

  另见 Unique Types[SRFI-137]

  和 [RnRK] 及 [RnRS] 的各种实现(如这里提到的)不同,对相同类型的封装对象,eqv?equal? 基于被封装对象的子对象(及子对象被引用的对象)递归比较,即使用封装的对象的 equal? 定义 eqv? 结果。

  另见等价谓词的设计用例

基础派生特性

  根环境特性中,除根环境基本特性的剩余接口是派生特性。其中在基础环境中提供的特性是基础派生特性(grounded derived feature)

基本派生特性

  基本派生特性可使用派生实现。这可能蕴含使用根环境基本特性或已在基本派生特性中提供的特性中的部分非派生实现。

模块约定:

  引入合并子的操作子对 <body> 的约定同合并子基本函数

  因互相依赖,一些操作实现为派生操作时,不能用于直接派生特定一些其它操作。

  和 $vau/e$vau/e%以及 $lambda/e$lambda/e%不同,不指定静态环境的合并子构造器隐含总是使用环境弱引用形式的静态环境,以避免过于容易引入循环引用

  本节约定以下求值得到的操作数

  • <box>箱(box) :可包含引用的容器。

注释

  注意 $let 等函数的 <body> 形式和 [RnRK] 不同。

操作:

eval <object> <environment>

  同 eval@ ,但 <object> 蕴含左值到右值转换且不保留引用值。

注释

  若 <object> 为元素中有引用值的列表,元素不会被特殊处理,不蕴含左值到右值转换

  [RnRK] 中同名合并子的第一参数为 <expression> ,但这不是已求值的操作数的类型。

eval% <object> <environment>

  同 eval@ ,但 <object> 蕴含左值到右值转换。

注释eval ,但保留引用值。

eval-string <string> <environment>

  同 eval-string ,但不保留引用值。

() get-current-environment

  取当前环境:取当前环境的弱引用

注释

  派生需要非派生实现的 $vau/e

() lock-current-environment

  锁定当前环境:取当前环境的强引用

$vau <formals> <eformal> <body>

  创建 vau 抽象

  类似 $vau/e,但以当前环境代替额外的求值环境作为静态环境。

注释

  和 [RnRK] 不同,可通过 $vau/e和(非派生的)get-current-environment 派生,不是基本操作

$vau% <formals> <eformal> <body>

  同 $vau,但保留引用值。

$quote <expression>

  求值引用操作。结果是返回值转换后的未被求值的操作数。

  考虑通常引用操作对符号类型未被求值的左值操作数使用,保留引用值没有意义,因此不提供对应保留引用值的操作。

id <object>

  结果是不蕴含左值到右值转换的参数,在结果中保留引用值。

  其作用等价返回值转换,可能引起对象转移

idv <object>

  同 id ,但结果是返回值转换后的值。

注释 使用 idv 可指定在函数值中保留引用值的不安全操作 的个别操作数不再保留引用值。

list <object>...

  创建列表(类型为 <list> )对象。

  list底层合并子接受 <pair>(而不要求是列表)作为函数合并对象,结果是操作数的元素经蕴含左值到右值转换的值。

注释 除元素的转换,类似 [RnRK] 的 list

$lvalue-identifier? <symbol>

  解析当前环境中的标识符(同 $resolve-identifier)并判断是否为左值(同 bound-lvalue?)。

$expire-rvalue <symbol>

  解析当前环境中的标识符,判断是否为左值(同 $lvalue-identifier? ),取对应的转换操作:

  • 若判断为左值,则转换操作同 id
  • 否则,则转换操作同 expire

  结果是在根环境求值转换操作对应的函数名得到的引用值。

注释 结果相同即以 eq? 和对应的值比较结果为 #t

forward! <object>

  转发可能是引用的值

  转移可修改的右值操作数(包括消亡值和临时对象)。

  其中,需转移时,使用使用项的转移。这和对象的转移不同,不保证调用宿主环境的转移构造函数。

原理 和宿主语言不同,直接转移允许区分消亡值和纯右值,同等地作为一等对象(如作为列表的元素)。

注释

  被转发的值若是形式参数树中的变量,一般应以带有标记字符 & 的形式绑定;否则,转发的不是对应的实际参数,而可能是其按值绑定的副本。

  这个函数类似宿主语言以对象类型参数和推断的函数参数类型作为模板参数调用 std::forward ,但若需转移,直接转移而非如 expire返回指定结果是消亡值唯一引用

list% <object>...

  同 list ,但每个参数都不蕴含左值到右值转换,在结果中保留参数的引用值

  list% 的底层合并子接受 <pair> 作为操作数(而不要求是列表),结果是操作数。

注释 类似 [RnRK] 的 list

rlist <list>

  转换参数为引用列表元素的列表。

  若参数是左值,则结果是参数的元素的左值引用值构成的列表;否则,结果同 idv

$remote-eval <expression> <environment>

  在动态环境求值第二参数得到的环境中求值第一参数,结果作为函数值。

$remote-eval% <expression> <environment>

  同 $remote-eval ,但保留引用值。

$deflazy! <definiend> <expressions>

  修改绑定。

  同 $def! ,但不求值参数;在添加绑定前仍对冻结环境进行检查

$set! <environment> <definiend> <expressions>

  设置:修改指定环境的变量绑定。

  在当前环境求值 <environment><body> ,再以后者的求值结果修改前者的求值结果指定的环境中的绑定。绑定效果同使用 `$def!

  类似 [RnRK] 的 $set! ,但明确使用 <definiend> 而不是 <formals> 。注意 <body> 的形式不同。允许的递归操作参见 $def!

  和 [RnRK] 不同而和 NPLA1 的 $def! 等类似,在修改绑定前对冻结环境进行检查。

$setrec! <environment> <definiend> <expressions>

  递归设置:修改指定环境的绑定,绑定效果同使用 $defrec!

  同 $set! ,但允许不同的递归操作。

注释 参见 $defrec!

$wvau <formals> <eformal> <body>

  创建包装的 vau 抽象

  同 $vau ,但创建的是调用时对操作数的元素求值一次的应用子

  参数的作用同 $vau 的对应参数。

$wvau% <formals> <eformal> <body>

  同 $wvau ,但允许函数体求值返回引用值。

$wvau/e <parent> <formals> <eformal> <body>

  同 $wvau ,但支持显式指定求值环境参数作为静态环境。

$wvau/e% <parent> <formals> <eformal> <body>

  同 $wvau/e ,但保留引用值。

$lambda <formals> <body>

  创建 λ 抽象

  同 $vau ,但创建的是调用时对操作数的元素求值一次的应用子,且忽略动态环境。

注释 可通过 vau 抽象或 $lambda/e 和(非派生的)get-current-environment 派生。

  除未提供的 <eformal> ,参数的作用同 $vau 的对应参数。

$lambda% <formals> <body>

  同 $lambda ,但允许函数体求值返回引用值。

$lambda/e <parent> <formals> <body>

  同 $lambda ,但支持显式指定求值环境参数作为静态环境。

$lambda/e% <parent> <formals> <body>

  同 $lambda/e ,但保留引用值。

list? <object>

  <list>类型谓词

  若参数是列表或非真列表,时间复杂度不大于 O(n) ,其中 n 是其中的元素数。

注释 本机实现可实现 O(1) 时间复杂度。

list* <object>+

  在列表前附加元素创建对象或有序对。

  类似 cons ,但支持一个和多个参数。

  对一个参数的情形结果同参数,否则结果同右结合嵌套调用参数的数量减 1 次的 cons

注释 一个参数的情形结果经返回值转换。因为不需要如 Kernel 支持派生 $vau ,可直接使用 apply 派生;但因底层合并子还需检查列表,所以可使用 apply-list 。而和 apply-list 内部共用实现的派生可能更高效。

list*% <object>+

  同 list* ,但创建有序对类似 cons% ,且元素保留引用值。

注释 一个参数的情形结果不经返回值转换。

apply <applicative> <object> <environment>?

  转发第一参数指定的应用子和第二参数指定的参数构成函数合并,在环境中应用。其中,环境是:

  • 新环境,若第二参数不存在。
  • 否则,第二参数指定的环境。

注释 检查 <environment> 和 [RnRK] 的参考派生不同。

  apply 的函数值保留引用值。

apply-list <applicative> <list> <environment>?

  转发第一参数指定的应用子和第二参数指定的参数列表构成函数合并,在环境中应用。其中,环境同 apply 中的方式指定。

  同 apply ,但首先检查第二参数的类型,若失败则引发错误

$sequence <expression-sequence>

  顺序求值。

  操作数非空时结果是最后的参数,可能是引用值。

注释 类似 [RnRK] 的同名操作子。

  求值每个 <object> 的副作用包括其中临时对象的销毁都被顺序限制。

注释 这类似宿主语言的语句而不是保证子表达式中的临时对象的生存期延迟到完全表达式求值结束的逗号表达式。这也允许实现和 [RnRK] 同名操作类似的 PTC 要求。

collapse <object>

  折叠可能是引用的值。

forward <object>

  转发可能是引用的非临时对象的值。

  同 forward! ,但对可修改的临时对象操作数,使用复制代替转移。

注释

  按在所在的环境中解析的操作数的类型可选地进行返回值转换作为结果,其作用 ididv 之一。

  转移(而不是复制)可修改的右值操作数。

  若右值操作数不可修改,复制不可复制构造的宿主对象会失败。

assign%! <reference> <object>

  同 assign@!,但 <object> 是引用值时赋值的源操作数是 <object> 折叠后的值。

assign! <reference> <object>

  同 assign%! ,但 <object> 蕴含左值到右值转换。

注释

  因为左值到右值转换,即便 <object> 指定的值来自 <reference> ,也可赋值而不因此引起未定义行为。

  另见赋值的注意事项

$defv! <variable> <formals> <eformal> <body>

  绑定 vau 抽象,等价 $def! <variable> $vau <formals> <eformal> <body>

$defv%! <variable> <formals> <eformal> <body>

  绑定 vau 抽象,等价 $def! <variable> $vau% <formals> <eformal> <body>

$defv/e! <variable> <parent> <formals> <eformal> <body>

  绑定指定静态环境的 vau 抽象,等价 $def! <variable> $vau/e <parent> <formals> <eformal> <body>

$defv/e%! <variable> <parent> <formals> <eformal> <body>

  绑定指定静态环境的 vau 抽象,等价 $def! <variable> $vau/e% <parent> <formals> <eformal> <body>

$defw! <variable> <formals> <eformal> <body>

  绑定包装的 vau 抽象,等价 $def! <variable> $wvau <formals> <eformal> <body>

$defw%! <variable> <formals> <eformal> <body>

  绑定包装的 vau 抽象,等价 $def! <variable> $wvau% <formals> <eformal> <body>

$defw/e! <variable> <parent> <formals> <eformal> <body>

  绑定包装的指定静态环境的 vau 抽象,等价 $def! <variable> $wvau/e <parent> <formals> <eformal> <body>

$defw/e%! <variable> <parent> <formals> <eformal> <body>

  绑定包装的指定静态环境的 vau 抽象,等价 $def! <variable> $wvau/e% <parent> <formals> <eformal> <body>

$defl! <variable> <formals> <body>

  绑定 λ 抽象,等价 $def! <variable> $lambda <formals> <body>

$defl%! <variable> <formals> <body>

  绑定 λ 抽象,等价 $def! <variable> $lambda% <formals> <body>

$defl/e! <variable> <parent> <formals> <body>

  绑定指定静态环境的 λ 抽象,等价 $def! <variable> $lambda/e <parent> <formals> <body>

$defl/e%! <variable> <parent> <formals> <body>

  绑定指定静态环境的 λ 抽象,等价 $def! <variable> $lambda/e% <parent> <formals> <body>

forward-first% <applicative> <list>

  取列表的第一元素并转发给指定的应用子。

  设参数列表 (&appv (&x .)) ,作用同求值:

(forward! appv) (list% ($move-resolved! x))

  其中,调用 appv底层合并子的当前环境同调用 forward-first% 的动态环境。

first <pair>

  取有序对的第一个元素的值。

  当 <list> 是左值时结果是折叠的引用值,否则结果是返回值转换后的值。

注释 类似传统 Lisp 及 [RnRK] 的 car 。命名和 [SRFI-1] 及 Clojure 等现代变体一致。

first@ <pair>

  同 first ,但结果不被折叠而总是未折叠的引用值

  首先同调用 check-pair-reference的方式检查参数是有序对引用,对右值引发错误。

first% <pair>

  同 first ,但结果总是转发的值。

  转发的值是经过折叠但没有返回值转换的列表元素的值,无论参数是否为引用值。

first& <pair>

  同 first@ ,但结果总是折叠的引用值。

  若元素是引用值,在结果中保留元素中的唯一引用属性。

原理 详见 NPLA1 引用值使用约定

firstv <pair>

  同 first ,但结果总是返回值转换后的值。

rest% <pair>

  取有序对的第一个元素以外的元素值经过转发的值构成的有序对。

  若结果构成子有序对,可能引入子有序对引用

rest& <pair>

  取有序对的第一个元素以外的元素值的引用值构成的有序对的子对象引用

  首先同调用 check-pair-reference 的方式检查参数是有序对引用,对右值引发错误。

  若结果构成子有序对,引入子有序对引用。

restv <pair>

  取有序对的第一个元素以外的元素值构成的有序对。

  结果是有序对对象。

set-first! <pair> <object>

  修改有序对的第一个元素。

  和 [RnRK] 的 set-car! 类似,但可派生,检查列表是左值,且不保留引用值。

set-first@! <pair> <object>

  同 set-first%! ,但保留未折叠的引用值

set-first%! <pair> <object>

  同 set-first! ,但保留引用值。

  不保证检查修改操作导致循环引用。

注释 用户应自行避免未定义行为

equal? <object1> <object2>

  判断一般相等。

  类似 eqv?,但同时支持表示中具有子项作为子对象的对象。

  判断的相等定义为子对象的递归相等性

注释

  类似 [RnRK] 和 [RnRS] 的同名的二元谓词,但在此保证可通过 eqv? 直接构造。:

  因为列表的性质,不需要支持循环引用,可以直接派生。后者被视为基本的抽象而非实现细节。

  和 make-encapsulation-type创建的对象的相等比较不同,本机实现和派生实现都依赖当前上下文,允许捕获续延,尽管续延是未指定的。

check-environment <object>

  检查环境

  若参数是 <environment> 则检查通过,结果是转发的参数;否则,引发错误对象

注释 当前实现中其它要求 <enviornment> 参数的操作中类型检查失败和 check-environment 失败的行为一致。

check-parent <object>

  检查作为环境的父环境的对象。

  若参数是可以作为合并子环境的 <parent> 则检查通过,结果是转发的参数;否则,引发错误对象。

  检查环境通过的条件同创建合并子时的检查

  引发错误对象的作用同创建合并子时环境检查失败引起错误或引发其依赖的错误对象(后者保证不是语法错误)。

$cond <clauses>

  条件选择。

  类似 [RnRK] 的同名操作子,但 <test> 的判断条件和 <body> 形式不同。

$when <test> <expression-sequence>

  条件成立时顺序求值。

  类似 klisp 的同名操作子,但若 <expression-sequence> 被求值,结果是 <expression-sequence> 的最后一个 <expression> 的求值结果,而不是 #inert

注释 这类似 [Racket] 的 when 而和 [R7RS] 的 when 或 klisp 的同名操作不同,因为 $when 被作为和 $sequence 类似的操作处理(对应 Racket 中的 whenbegin 并列)。

$unless <test> <expression-sequence>

  条件不成立时顺序求值。

  类似 klisp 的同名操作子,但若 <expression-sequence> 被求值,结果和设计原理同上。

not? <object>

  逻辑非。

  被求值的参数同 <test> ,进行左值到右值转换。

  若参数非 #f 时结果是 #f ,否则结果是 #t

注释 和 [RnRK] 不同而和 Scheme 类似,视所有非 #f 的值为 #t

$and <test>...

  逻辑与。

  顺序短路求值。操作数为空时结果是 #t ;参数求值存在 #f 时结果是 #f ;否则结果是最后一个参数的值。

  结果保留引用值。

原理

  [RnRK] 的 $and?$or? 的实现使用 applywrap ,这没有必要:

  • 按 [RnRK] 的 apply 的原理,这种对任意合并子适用的操作 combine 容易实现且干扰意图的理解。
  • 对 NPLA1 的 apply ,还保证在第一参数是空列表时,为适应合求值函数合并(前缀 (),被继续求值的对象仍是有序对(即函数合并,而不是单独的函数),但这在 NPLA1 的 $and$or 中不必要,因为对应的情形(即 $and$or 没有参数时)应被单独处理。
  • 对派生实现,apply 通常比 eval% 更低效(因为包含了无用的检查和更多的非本机实现)。

注释

  和 [RnRK] 的 $and? 不同,不检查类型,也不保证结果类型是 <boolean> ,所以命名不以 ? 结尾

  和 [RnRK] 中的原理描述的不同,这同时允许直接的满足 PTC 要求的派生实现。

$or <test>...

  逻辑或。

  顺序短路求值。操作数为空时结果是 #f ,参数求值存在不是 #f 的值时结果是第一个这样的值;否则结果是 #t

  结果保留引用值。

原理

  参见以上 $and 的原理。

注释

  和 [RnRK] 的 $or? 不同,具体差异参见以上 $and 的注释。

accl <object1> <predicate> <object2> <applicative1> <applicative2> <applicative3>

  在抽象列表的元素上应用左结合的二元操作。

  对 <object1> 指定的抽象列表进行处理,取得部分和。

  当谓词 <predicate> 成立时结果是 <object2> ,否则继续处理抽象列表中余下的元素。

  处理抽象的列表的操作通过余下的应用子分别定义:取列表头、取列表尾和部分和的二元合并操作。

  参数 <applicative1> 和参数参数 <applicative2> 应接受两个参数,否则引起错误

  参数 <applicative3> 应接受两个参数,否则引起错误。

  调用参数中的应用子的 <object1> 实际参数在不同的应用子调用中可能同一

  调用参数中的应用子的底层合并子的当前环境同调用 accl 的动态环境。

accr <object1> <predicate> <object2> <applicative1> <applicative2> <applicative3>

  在抽象列表的元素上应用右结合的二元操作。

  操作方式同 accl

  和 accl 不同,可保证合并操作是尾调用;相应地,递归调用不是尾上下文而无法满足 PTC 要求。

foldr1 <applicative> <object> <list>

  作用同符合以下要求的 accr 调用:

  • 指定 accr 的参数为 <list>null?(forward! <object>)first%rest%<applicative>
  • 调用应用子 rest% 时不复制 <object> 或其子对象。

  参数指定的应用子的调用不添加或移除列表元素,否则行为未定义

注释

  类似 [SRFI-1]) 的 fold-right ,但只接受一个真列表

  foldr1 名称中的 1<list> 参数的个数。(更一般的其它形式可接受多个 <list> 。)

map1 <applicative> <list>

  单列表映射操作:使用指定应用子对列表中每个参数进行调用,结果是其返回值的列表。

  参数 <applicative> 应接受一个参数,否则引起错误。

  操作中的应用子和列表构造的结果的确定满足过程调用的因果性;其余任意 <applicative> 调用的求值、列表构造操作和销毁列表中的元素的操作的相对顺序未指定。

  <applicative> 的调用不添加或移除列表元素,否则行为未指定。

注释

  类似 [RnRK] 的 map ,但只接受一个真列表

  map1 名称中的 1 的含义类似 fold1

first-null? <list>

  复合 firstnull? 操作。

rulist <list>

  转换参数为可带有唯一引用的引用列表元素的列表。

  同 rlist ,但在参数是左值时,参数中的非引用值元素在结果中对应转换为其唯一引用。

注释 消亡值处理和 rlist 不同。

list-concat <list> <object>

  顺序连接列表和对象。

注释 当且仅当 <object> 实际参数是 <list> 值时,结果是 <list> 值。

append <list>...

  顺序连接零个或多个列表。

注释 若没有参数,结果是空列表。

list-extract-first <list>

  以 first 在参数指定的 <pair> 的列表中选取并合并内容为新的列表。

  设参数列表 (&l) ,结果同在新环境中求值表达式 map1 first l ,其中 map1first 是标准库函数。

list-extract-rest%! <list>

  以 rest% 在参数指定的 <pair> 的列表中选取并合并内容为新的列表。

  设参数列表 (&l) ,结果同在新环境中求值表达式 map1 rest% l ,其中 map1rest% 是标准库函数。

list-push-front! <list> <object>

  在列表前插入元素。

  要求 <list> 可修改,否则类型检查失败。

  参数 <object> 被转发。

$let <bindings> <body>

  局部绑定求值:创建以当前环境为父环境的空环境,在其中添加 <bindings> 指定的变量绑定,再求值 <body>

  在添加绑定前,<bindings> 中的初值符被求值。

  返回非引用值

注释 类似 [RnRK] 的同名操作子,但返回非引用值。

$let% <bindings> <body>

  同 $let ,但保留引用值。

$let/e <parent> <bindings> <body>

  指定静态环境并局部绑定求值。

原理

  显式控制 <parent> 以允许传递引用值并在外部确保环境(可以是环境强引用)被正确传递作为求值的父环境,而无需支持扩展 <parent> 中的环境为右值时其中的环境临时对象的生存期

  注意此时 <parent> 中的环境中创建的环境对象在表达式求值后仍会因引入的合并子生存期结束而被销毁。

注释

  类似 [RnRK] 的 $let-redirect ,但使用 $lambda/e 而非 $lambda 作为抽象且支持 <parent> ,并返回非引用值。

$let/e% <parent> <bindings> <body>

  同 $let/e ,但使用 $lambda/e% 而非 $lambda/e 创建抽象,保留引用值。

$let* <bindings> <body>

  顺序局部绑定求值。

  同 $let ,但 <bindings> 中的被用于绑定的表达式从左到右顺序求值,被用于初始化变量的表达式在求值时可访问 <bindings> 中之前绑定的符号。

注释 类似 [RnRK] 的同名操作。

$let*% <bindings> <body>

  同 $let* ,但保留引用值。

$letrec <bindings> <body>

  允许递归引用绑定的顺序局部绑定求值。

注释 类似 [RnRK] 的同名操作。

  和 $let$let* 不同,操作求值 <bindings> 的初值符时保证使用和求值 <body> 时的同一环境作为当前环境,因此可配合 lock-current-environment 传递具有所有权的环境。

$letrec% <bindings> <body>

  同 $letrec ,但保留引用值。

derive-current-environment <environment>...

  创建当前环境的派生环境:以参数指定的环境和当前环境为父环境空环境

  当前环境以外的父环境顺序同参数顺序。当前环境是最后一个父环境。

() make-standard-environment

  创建标准环境(standard environment) :以基础环境为父环境的空环境。

  类似 [RnRK] 的 make-standard-kernel-environment ,但创建的环境基于 NPLA1 基础环境。

注释 标准环境同 [RnRK] 约定的定义。

derive-environment <environment>...

  创建基础环境的派生环境:以参数指定的环境和基础环境为父环境的空环境。

  当前环境以外的父环境顺序同参数顺序。基础环境是最后一个父环境。

  创建的环境是标准环境,当且仅当没有实际参数。

注释 类似 make-standard-environment ,但具有参数指定的环境作为其它的父环境。

$as-environment <body>

  求值表达式以构造环境。

  创建以当前环境为父环境的空环境,并在其中求值参数指定的表达式。

  结果是创建的环境强引用。

$bindings/p->environment (<environment>...) <binding>...

  转换绑定列表为以指定的环境列表中的环境为父环境的具有这些绑定的环境。

  类似 [RnRK] 的 $binding->environment ,但指定父环境,且具有适当的所有权。

  使用 make-environment而不是 $let/e 等绑定构造实现。

$bindings->environment <binding>...

  转换绑定列表为没有父环境的具有这些绑定的环境。

注释 类似 [RnRK] 的同名操作子,但因为要求对内部父环境环境所有权,使用 $bindings/p->environment 而不是 $let/e 等绑定构造派生。

symbols->imports <symbol>...

  转换符号列表为未求值的适合初始化符号导入列表的初值符列表。

  结果是包含同 desigil的方式移除标记字符后的参数作为间接子项的列表。

  求值这个列表,结果是同 forward! 的方式转发每个符号的列表,其元素顺序和 <symbols>... 中的值的顺序对应。

  结果的结构和使用满足以下约定:

  • 结果中可能存在合并子作为其子对象,其包装数未指定。
  • 取结果中的子对象进行求值的行为未定义。
  • 若结果被修改(如被转移),再求值时行为未定义。
  • 若结果中的合并子在求值整个结果外的上下文被调用,行为未定义。

原理 这些约定可允许更有效的本机实现。

注释

  类似 [RnRK] 的 $provide!$import! 提供符号列表的方式,但有以下不同:

  • 支持移除引用标记字符。
  • 支持转发参数。
  • 不带有引用标记字符和符号指称的对象不是临时对象的默认情形复制值而不是初始化引用。

$provide/let! <symbols> <bindings> <body>

  指定局部绑定后在当前环境中提供绑定。

  蕴含 $let <bindings> <body> ,在求值 <body> 后以结果作为操作数绑定到 <symbols> 的符号。

  <symbols> 应能被作为 <definiend> 使用。

  结果是对这些绑定具有所有权的环境强引用。

  需要导入符号,即 <symbols>... 具有至少一个实际参数时,以同 symbols->imports 的方式确定初值符。其中,等效的 symbols->imports 的调用次数未指定。

注释 绑定后的符号可通过作为 vau 抽象的父环境等形式依赖这个环境,因此用户需适当保存返回值使其生存期覆盖在被使用的绑定符号指称的对象生存期。

$provide! <symbols> <body>

  在当前环境中提供绑定。

  同 $provide/let! ,但不指定单独的 <bindings>

  作用同 <bindings> 为空列表的 $provide/let!

  结果是创建的环境的强引用。

  需要导入符号时,以同 symbols->imports 的方式确定初值符。其中,等效的 symbols->imports 的调用次数未指定。

注释 类似 [RnRK] 的同名操作子,但结果是创建的环境的强引用,且确定初值符的方式被显式要求。

  仅当 <symbols> 类型检查通过时求值 <body>

  检查当前环境可修改失败时的副作用和以上任一等效求值 symbols->imports 应用子的结果可能具有的副作用非决定性有序

$import! <environment> <symbol>...

  从指定的环境导入指定的符号。

  对第一参数之后的其余参数指定的符号列表中的每个符号,修改第一参数指定的环境,创建和指定的符号具有相同的名称和值的变量绑定。

  类似 [RnRK] 的同名操作子,但需要导入符号时,以同求值 symbols->imports 应用子的结果的方式确定初值符。其中,等效的 symbols->imports 的调用次数未指定。

  当指定的环境中的指定符号对应的绑定以临时对象创建时,导入符号可修改指定的源环境的被绑定对象。

注释 由于求值 symbols->imports 应用子的结果蕴含的转发语义,这和 [RnRK] 不同。

  检查 <environment> 可修改失败时的副作用和以上任一等效求值 symbols->imports 应用子的结果可能具有的副作用非决定性有序。

$import&! <environment> <symbol>...

  从指定的环境以引用绑定导入指定的符号。

  同 $import! ,但以 ensigil的方式指定绑定的符号。

nonfoldable? <list>

  判断参数是否不可被继续折叠映射:存在空列表。

  参数是同 [RnRK] 的 map 操作可接受的列表参数或空列表,但排除非真列表

  若参数是空列表,结果是 #f

assq <object> <lists>

  取关联列表中和参数的引用相同的元素。

  第二参数指定的列表中的元素应为有序对。

  以 eq? 依次判断第二参数指定的列表中的每个元素的第一个元素是否和第一参数指定的元素等价。

  若不存在等价的元素,结果为空列表右值;否则是同 first% 访问得到的等价的列表的值。

原理

  和 [RnRK] 不同,NPLA1 只支持真列表,因此可以要求顺序,提供关于等价的元素的更强的保证。

  尽管和 [RnRK] 相同而和 [RnRS] 不同,<test> 支持非布尔值,不存在元素时的 #f 结果可以简化比较,但和 [RnRK] 的原理类似,这不利于提供清晰的类型错误,且没有如空列表值这样作为求值算法的特殊值的自然推论。使用空列表值和传统 Lisp 也一致。

注释

  类似 [RnRK] 的同名应用子,但保证顺序且转发参数。

  类似 [RnRS] 的同名过程,但失败的结果不是 #f

assv <object> <lists>

  取关联列表中和参数的值相等的元素。

  第二参数指定的列表中的元素应为有序对。

  以 eqv? 依次判断第二参数指定的列表中的第一个元素是否和第一参数指定的元素等价。

  若不存在等价的元素,结果为空列表右值;否则是同 first% 访问得到的等价的列表的值。

原理 参见 assq

注释

  类似 [RnRK] 的 assoc ,但使用 eqv? 而不是 equal? ,保证顺序且转发参数。

  类似 [RnRS] 的 assv ,但失败的结果不是 #f

box <object>

  装箱:构造参数对象经左值到右值转换的箱(类型为 <box> 的对象)。

box% <object>

  同 box ,但参数不蕴含左值到右值转换,在结果中保留参数的引用值

box? <object>

  <box>类型谓词

unbox <box>

  拆箱:从箱中还原对象。

  作为函数值转发操作,保留引用值。

注释

  以上 4 个函数除引用标记字符对应处理引用值的差异外,功能和使用方式对应类似 [SRFI-111] 的 3 个过程 boxbox?unbox

  类型分区使 box?<list> 类型的参数的结果总是 #f 。若没有这个限制,不考虑封装性时,用 <list> 的相关操作可整体替换进行功能等价的代替:listlist%first 可代替 boxbox%unbox

  和 关于 Scheme 的装箱的描述不同,这样的代替不一定保证有更好的性能。

  以上这些函数可使用 make-encapsulation-type实现。

  和 Scheme 等不同,箱具有被装箱对象的所有权,因此使用 box%unbox 时,需注意保存被构造的箱或被箱中引用值引用的对象。

标准派生特性

  标准派生特性(standard derived feature)基本派生特性,但其派生依赖标准库其它模块

ensigil <symbol>

  修饰引用字符。

  若参数非空且没有 & 前缀,则结果是添加 & 引用标记字符作为前缀的符号;否则是参数值。

$binds1? <environment> <symbol>

  判断指定符号是否在指定表达式求值后指称的环境中绑定。

注释 类似 [RnRK] 的 $binds? ,但只支持判断一个 <symbol> 操作数。

核心库

  核心库(core library) 提供以下操作,即核心库函数:

map-reverse <applicative> <list>...

  映射并反转结果。

  参数 <applicative> 应满足以下要求,否则引起错误

  • <list>... 中的参数的元素数都相等。
  • <list>... 中的参数的元素数量等于 <applicative> 接受的形式参数的元数。

注释 类似 [RnRK] 的 map ,但支持空的 <list>... 且保证顺序。

for-each-ltr <applicative> <list>...

  从左到右映射取副作用。

注释 类似 [RnRK] 的 for-each ,但支持空的 <list> 且保证顺序。

NPLA1 参照实现扩展环境

  类似 NPLA1 根环境特性,NPLA1 以根环境的形式提供其它一些模块的操作,但默认不以根环境中的绑定而是以其中的环境子对象中的绑定提供。

  除非派生实现另行指定,这些模块都应被提供。

  这些模块和 NPLA1 参照实现环境提供的特性一同构成标准库

  除非派生实现定义,每个标准库模块都不是可选的。否则,作为被加载的模块的环境的名称由派生实现定义。

  修改这些环境的程序行为未定义

  默认加载使用 . 分隔标识符得到的符号作为名称。

  加载的模块依赖初始化的根环境

  当前实现中部分加载的环境依赖之前加载的环境,这些环境的名称是固定的。用户程序需要保证这些环境在加载时的静态环境中可用。

  在调用其中的合并子时,可能求值符号引用依赖的环境。其中的环境可能在求值定义时不依赖而不作为对应的本机 API 的前置条件。

  环境是否具有依赖的环境的绑定绑定是未指定的。

  用户程序需保持加载为环境的模块具有适当的生存期,以避免其中的合并子调用引起未定义行为。

  本章的特性可使用本机实现非本机的派生实现,分别符合根环境基本特性基础派生特性的规则。具体使用何种实现是未指定的。

  派生实现可定义更具体的实现要求,以便互操作的兼容性。

原理

  这些绑定不需要被直接引入基础上下文的根环境中,因为:

  • 同时满足以下关于接口依赖的约束,而不必要以基础环境可访问的名称提供:
    • 它们不是使用作为环境的模块时被依赖的主要操作。
      • 为了使用非根环境的模块,需要绑定在根环境的函数引入其中的绑定。这样的接口应在根环境中提供而保证默认可访问,避免引入绑定这样的功能的在逻辑上的循环依赖。
      • 注释 这样的操作如 $import!
    • 它们不被根环境特性的在接口意义上依赖。
  • 接口的功能不对一般的实体具有足够普遍性,而不需要以基础环境默认可访问的名称提供。

  判定上述的足够普遍性的具体规则包括:

  • 普遍性以实体类型体现为接口的功能对非特定类型的对象适用,最终不依赖具有更特定的类型特有的性质。
    • 注释 一般的实体作为一等对象,即具有 <object>的值。<object> 是足够普遍的类型。
    • 注释 <reference><box>等由 <object> 构造的值最终依赖 <object> 的值,而非其它更特定类型特有的性质。
  • 求值算法中出现决定具体步骤的具体实体类型,被认为是足够普遍的。
  • 若接口的功能仅依赖比一般的实体更具体的特定类型的值,则不以基础环境默认可访问的名称提供。
    • 注释 功能上的依赖包含区分这些特定类型的值,如类型谓词
  • 接口的功能描述涉及的类型的足够普遍性对以基础环境默认可访问的名称提供是必要非充分条件。
    • 这些类型仅决定接口功能的一部分。
    • 注释 若接口的功能仅依赖足够普遍的类型,但功能不足以涵盖它的任何的子类型或者值,也可在参照实现扩展环境中提供。
    • 注释 一个主要特例:足够普遍的具体类型的类型谓词涵盖所有值,因此类型的足够普遍性可直接作为对应的类型谓词的足够普遍性的充分必要条件。

  具有足够普遍性而应在根环境而非参照实现扩展环境提供的操作具体包括以下几类:

  • 创建非特定的不同名义类型的对象使用的普遍机制的主要操作。
  • 不改变一般的实体可能蕴含的对象同一性而同时附加非特定种类副作用的操作。
    • 同一性是所有对象的属性。显示同一性不依赖具体副作用的种类,因此要求特定种类的接口削弱普遍性。另见可变状态和普遍性
    • 注释 根环境中这样的操作如 box%unbox
  • 不依赖特定对象类型,直接引入副作用的操作。
    • 因为引入副作用可能是接口的关键功能及主要目的,此处的普遍性不限制非特定种类。
    • 注释 仅具有控制作用为副作用的操作仍被视为是普遍的。因此,可具有控制作用$if等函数仍在根环境中提供。
    • 注释 依赖一等续延控制作用因续延类型而不视为足够普遍,因此根环境不直接提供一等续延关联的操作。

  在此基础上,这些绑定被设计为环境子对象提供的子模块,因为以下的一个或多个原因:

  • 它们可能具有非全局含义的名称而更适合隔离在不同命名空间中以避免使用时的歧义。
  • 它们可能仅关注(如作为操作的参数或返回类型)特定的求值得到的操作数类型。
  • 它们中的每一个模块具有足够或内聚性而不和其它子模块耦合,且绑定提供的实体关注相同的功能集合,适合被派生实现直接配置为可选的(#根环境基本特性)特性分组。
  • 允许实现使用特殊的环境子类型延迟加载
    • 注释 当前 NPLA1 没有提供支持。

注释

  类似根环境特性,一些特性可约定处理的值的宿主类型

  使用 . 分隔标识符得到的符号类似 CHICKEN Scheme 的转换 R7RS 的标准模块名

续延库

  提供续延支持。

  默认加载为根环境下的 std.continuations 环境。

模块约定:

  本节约定以下求值得到的操作数:

原理

  类似 Kernel 语言,NPLA1 续延不是合并子。这一设计的基本原理参见 [RnRK] §7(以及续延的捕获和调用),但理由不充分。更完整的分析如下:

  • 使用守卫选择子(guard selector) 是 Kernel 的具体特性的设计,不是一般求值算法中蕴含需要实现的选项,因此仅在 [RnRK] 内部生效。
    • 这和类似的其它机制(如 [RnRS] 的 dynamic-wind thunk )事实上都同以下关于续延组合性的理由类似,不需要单独列出。
  • 如 [RnRK] ,一般的合并子确实无法保证作为续延的父续延(parent continuation) 。
    • [RnRK] 在此没有进一步明确:
      • 一般的合并子无法作为父续延的原因是因为(作为函数)不保证能接受操作数。
      • 父续延对续延组合(接受两个续延结果是和两者连续调用等效的续延)操作是必要的输入。
      • 更一般地,即便不支持续延组合操作,在抽象机语义描述续延的捕获得到一等续延依赖这种语义(即便父续延不是一等对象)。
        • 只要描述依赖可组合的求值算法,除了最后一个无界续延,其它续延都对应可组合的求值步骤,因此这隐含续延的可组合性。
        • 其它在求值算法以外的机制的描述也可能依赖这种组合性,如:https://docs.racket-lang.org/reference/eval-model.html#(part._mark-model) 。
        • 最后一个无界续延可不考虑可组合性。但只要它不是唯一的,为作为一等续延被直接捕获和调用,它仍应当接受程序指定的操作数。
          • 仅当这个续延唯一时可不提供单独的续延类型。
            • 此时,这个续延被隐含而不当作一等续延,其中的操作数直接通过非续延的函数直接表示,如 [ISO C] 标准库的 exit 函数。
            • 然而这样的设计要求其它续延是可组合的,否则根本不支持一等续延。
          • 可组合续延时,这个唯一的续延会被这作为被组合的其它续延的公共后缀。
            • 此时仍然需要支持指定操作数,以便最后一个续延能表现程序指定的不同行为;否则,显式提供这样的续延缺少实际意义。
          • [RnRK] 要求 root-continuationerror-continuation 这两个不同的一等无界续延能作为后缀,因此不是唯一的。
    • 所以,为符合正确性,一般的合并子不是续延的子类型
  • 因为包装是独立在具体合并子类型外的操作,[RnRK] 说明了一般续延不应作为应用子的子类型,而这并没有有效说明续延无法作为操作子的子类型。
  • 一些观点认为无界续延不能作为函数。但这实际依赖具体对象语言的规则,同样无法说明一般的续延无法作为操作子的子类型。
    • 典型的分析如参见这里
    • 这仅论述了续延界限和续延(调用)的某种可组合性(composablility) 的要求之间的关系。
      • 特别地,此处的可组合性局限于取得函数值。
    • 因为这种可组合性的限制,这隐含一个前提:函数不能不返回。这对一般的函数并不成立,因为一般的函数允许存在副作用,这种副作用不一定局限于蕴含此处可组合的控制作用
    • 在类型系统中,不返回的函数仍可能是良型的,因为函数类型的构造器是否接受空类型和具体类型系统的设计相关。
      • 类型系统规则能保证编码可组合的函数的机制是确保语法上可构造可组合的嵌套函数调用(函数值可作为另一个函数的实际参数),以此构成传统数学函数复合的自然扩展,并保证作用可复合,但这不保证函数值可复合。
        • 对总是不返回的函数,非终止(non-terminization) 是其作为后缀的一种单一副作用,吸收(absorb) 任何返回函数值的这一计算作用。这破坏函数调用具有返回值的预期,但并非在作用上不可组合。
      • 事实上,不提供一等控制作用机制的语言也可能允许这种类型规则。
        • 如 [ISO C] 的 _Noreturn 函数实质上返回类型就是空类型(因为没有任何值可作为返回值居留),尽管 C 的类型系统编码中 _Noreturn 函数返回类型并不唯一(返回类型这在转换等其它类型检查中仍然有效)且空类型不被检查(违反 _Noreturn 约束是未定义行为)而可能显得不典型。
        • 注释 [ISO C] 仅提供 setjmp/longjmp 改变通常的控制状态。此外,[ISO C++] 的类似的 [[noreturn]] 用于标注总是抛出异常的函数,而提供异常的(替代)实现是一等控制操作符的一个典型使用场景。
    • 根本地,使用这种值而非一般计算作用的可组合性定义的理由,仅来自某种描述语言规则的元语言上推理的要求或约定。
      • 注释 例如,为了便于使用等式理论(equational theory) 进行推理,证明被描述的对象语言总是符合某种静态的性质。
      • 一般地,基于目的的不确定性,元语言不总是遵循这种约定。
      • 对象语言更没有必要遵循这种约定,因为这蕴含对对象语言功能完整性的任意地、不必要的限制。
      • 避免非预期的终止性可很容易通过对维护外部语言实现环境的运行时的互操作实现:添加一个破坏维护非终止的运行时条件的操作(例如,撤除硬件电源),即便需要按某种方式保留运行时状态,通常仍远远比提供静态的证明更容易。
      • 此外,不论对象语言是否有必要表达,普遍的组合性不总是有益的,自身可能不被预期。
        • 易于排除非终止这种计算作用的实现方法,正好是得益于语言实现和外部系统之间的计算作用不能自发维持组合性的直接应用。
  • 实际决定对象语言中区分续延和合并子(且续延明确不使用合并子表示)的设计有不同的其它理由:
    • 求值算法对合并子的处理决定合并子对非正常控制透明。作为引入非正常控制的机制,续延调用和遵循 β-规约的合并子调用具有不同来源的语义规则,即便后者在元语言中可能实现前者。
      • 因此,是否把续延表示为传统的过程或合并子等其它蕴含 β-规约的实体是实现细节。抽象上,这支持保持续延的表示不透明而和合并子相互独立。
      • 对一等续延相关的控制操作的一种一般的语义描述参见这里
    • 避免把续延作为合并子还有语用因素。具体地,续延调用通常使用续延应用的形式,而非操作子调用。续延应用不直接复用函数合并的语法。
      • 以操作子的方式类似进行续延调用可能依赖对象的内部表示而暴露不必要公开的抽象,并不常用,也并不被当前语言普遍支持。
        • 注释 大多数语言根本不支持操作子。
        • 但禁用这种调用,在一等续延的调用规则中添加偶然耦合,阻碍功能完整性和设计的简单性
      • 考虑到捕获的一等续延通过变量绑定提供,因为合并子不是应用子的子类型,若一等续延是合并子,只能是指称操作子的变量。
        • 于是,程序中的续延应用总是要求对其包装后使用。
        • 这种设计使程序的实现和简单性冲突,并违反多个结构设计规则,而削弱提供这些绑定的续延捕获特性的可用性
      • 因此,一般的续延不是应用子的子类型,程序中一等续延的应用需转换续延为应用子。
      • 推论:一般的续延不是合并子的子类型。
  • 综上,结合以上合并子不是续延的子类型以及续延不是合并子的子类型,一般的续延和合并子类型是正交的。

  NPLA1 首先提供关于一等续延而不是一等协程的控制操作,因为:

  • 协程要求支持具有更多的原语,包括基于(而非并行于)一般的例程的创建操作(构造器)。
  • 具有相等的一等协程的内部表示逻辑上更加复杂(总是涉及可变状态),即便具体实现并不一定更复杂(和内部表示相关)。
  • 使用非对称协程派生对称协程比使用有界续延派生无界续延在逻辑上依赖暴露更多的细节,如包含分支的跳板循环。

  尽管逻辑上有界续延能不依赖其它副作用而表达状态,提供有界续延或无界续延作为原语的差异相对次要,因为:

  • NPLA1 不是纯函数式语言而支持一等状态在内的基本设计,在实现中不需要排除可变状态。
  • NPLA1 不提供特设的和续延并行的异常或者其它替代的非正常控制机制,而不具有控制作用之间的互操作难以组合的问题
  • 尽管逻辑上引入续延界限可能是有益的,且有微妙的语义上的效果,这并没有简化用户程序和语言实现,因此当前不要求。
    • 当前不支持多次续延和续延复制。
      • 当续延仅在同一个上下文中捕获时,缺少续延界限不会是一个明显的问题,因为被调用的续延总是会重新引入共享的子续延。
    • 一些当前语言设计在 [RnRS] 的操作上扩展了界限。
    • 缺少续延界限可能引起控制操作符关联的一些模型之间的语义不等价问题而难以使用其中的一种严格准确地表达其中的另一种
      • 在 SML/NJ 等语言中,因为不存在顶层的续延界限,这些问题容易成为在程序中真正阻碍使用有界续延的困难。
      • 在 [RnRK] ,正常程序运行的顶层续延界限以 root-continuation 提供。此外,提供 error-continuation 处理错误。
      • 在完善的设计中,提供一个顶层的续延界限并非困难。因此,可能缺少取得续延界限的方法的问题不是核心困难。此外,如 [RnRK] 的 error-continuation 显示了其它类似的续延的实用性。
        • 类似的特性可能会被加入此处的设计中。

注释

  类似 [RnRK] 而和 [RnRS] 不同,续延具有单独的类型,续延应用也不是蕴含过程调用的函数应用。

操作:

call/1cc <combiner>

  捕获一次续延具现为一等续延作为参数调用合并子。

  续延首先在尾上下文中按引用捕获,再作为操作数,调用 <combiner>

  来自同一具现的一次续延的任何副本只允许一次显式(续延应用)或者隐式(如被函数调用的返回蕴含)地成功调用;否则,调用被重入引起错误

  捕获的续延之后允许被复制。

注释

  call1/cc 的名称来源同 Chez Scheme 。

continuation->applicative <continuation>

  转换续延为关联的等效应用子。

  结果是转换的 <applicative> 类型的值。

  在构造结果时,<continuation> 被转发。

  结果的底层合并子被调用时,传递操作数树给 <continuation>

apply-continuation <continuation> <object>

  应用续延。

  以第二参数作为参数,以 apply 应用第一参数指定的续延转换的应用子。

原理

  同 [RnRK] ,apply-continuation 不接受可选的环境,因为非正常控制忽略动态环境。

注释

  即同求值:apply (apply-continuation <continuation>) <object>

  apply-continuation 同 [RnRK] 。取得非 <list> 结果依赖 apply<pair> 的支持,这在 Scheme 中无法实现。

代理求值

  代理求值支持保存求值为代理对象以实现延迟求值。

  默认加载为根环境下的 std.promises 环境。

模块约定:

  本节约定以下求值得到的操作数:

  • <promise> :求值代理:表示可被求值而取得结果对象的对象。

原理

  代理求值的原语可实现惰性求值和透明的记忆化。

  和一些流行的误解不同,尽管这些原语的原始设计是关于并行处理的,这不必然蕴含并发的投机执行(speculative execution) ,只是因为解析(resolve) 内部状态并不在用户程序中可见,而蕴含必要的最小同步。

  由于当前语言不支持并发访问,即使对 <promise>修改操作导致变化,在语言中其状态也不可见,没有要求支持这种同步;未来可能会附加要求以提供更完善的并发支持。

  关于 API 的设计,参见 [RnRK] §9 和 [SRFI-45]

注释

  在 <promise> 上的并发访问并不具有特别的同步保证和要求。

  除 $lazy/d 外,同 [RnRK] 的 promises 模块。

  和 [RnRK] 不同,通过 force 引起 <promise> 对象的求值可能修改这个对象自身而使其中的状态失效(如通过 assign!对这个对象赋值)。

  因此,实现中需要重新访问状态,而重新进行类型检查。

操作:

promise? <object>

  <promise>类型谓词

memoize <object>

  记忆化求值:以参数作为已被求值的结果创建 <promise> 对象。

  在结果中保留参数的引用值

$lazy <body>

  惰性求值:以参数为待求值的表达式,以当前环境作为这个表达式被求值的动态环境,创建 <promise> 对象。

  当前环境的环境弱引用的副本被保存到结果。

$lazy% <body>

  同 $lazy ,但保留引用值。

$lazy/d <environment> <body>

  同 $lazy ,但以参数指定环境替代动态环境。

$lazy/d% <environment> <body>

  同 $lazy% ,但以参数指定环境替代动态环境。

  参数指定的一等环境值的副本被保存到结果。

force <object>

  若参数是 <promise> 值,立即求值指定的 <promise> 对象,得到的结果对象作为结果;否则,转发参数作为结果。

  若参数在求值时修改为非 <promise> 类型的值,需要继续迭代求值时,引起类型错误

数学库

  数学库提供数值类型的操作和其它数学功能。

  默认加载为根环境下的 std.math 环境。

模块约定:

  以下操作中:

原理

关于比较操作:

  同 [RnRK] 而不同于 [R5RS] 、[R6RS] 和 [R7RS] ,比较操作不明确要求传递性(但精确数仍然因真值的数学性质而保证传递性),以允许操作数存在不精确数时,转换不精确数 flonum 的简单实现。

  [R6RS] 继承了 [R5RS] 要求过程 = 具有传递性(注释指出传统类 Lisp 语言的实现不要求),而 [R7RS] 的对应注释指出这不能通过把所有操作数都转换为不精确数实现。

  不要求不精确数的传递性和除法约定对除数为不精确数 0 值的处理兼容。

  不同的语言在此可能有不同的规则,可见:

  • 一些现代 Lisp 语言可能满足(即便不是 Scheme 实现,如 SBCL )或不满足(即便是 Scheme 实现的派生,如 Racket )此要求。
  • 一些语言的不同版本的实现可能使用不同的规则(如 Ruby 1.8.7 和 Ruby 2.0 )。

  使用以上链接中的测试用例,可发现一些 [RnRS] 的实现实际可能不符合 = 的传递性要求,如 x86-64 Linux 上:

  • Chez Scheme 9.5.6 、Chibi Scheme 0.10.0 和 Gauche 0.9.11 不符合要求。
  • Chicken 5.3.0 、Guile 2.2.7 和 Gambit 4.9.4 符合要求。

  上述符合性问题已在以下实现中报告并被修复:

注释

  和数值操作约定不同,幂等操作要求超过一次应用时,结果和参数的宿主类型也相同。

操作:

number? <object>

  <number>类型谓词

complex? <object>

  <complex> 的类型谓词。

注释number? ,因为当前 <number> 值都是 <complex> 值。

real? <object>

  <real> 的类型谓词。

注释complex? ,因为当前 <complex> 值都是 <real> 值。

rational? <object>

  <rational> 的类型谓词。

注释 当前实现仅需排除无限大和 NaN 值

integer? <object>

  <integer> 的类型谓词。

exact-integer? <object>

  判断参数是否为 <integer> 类型的精确数对象。

fixnum? <object>

  判断参数是否为 fixnum对象。

flonum? <object>

  判断参数是否为 flonum对象。

exact? <number>

  判断参数是否为精确数。

注释 当前精确数都是 fixnum 。

inexact? <number>

  判断参数是否为不精确数

注释 当前不精确数都是 flonum 。

finite? <number>

  判断参数是否为有限值

infinite? <number>

  判断参数是否为无限大值。

nan? <number>

  判断参数是否为 NaN 值。

=? <number1> <number2>

  比较相等。

<? <real1> <real2>

  比较小于。

>? <real1> <real2>

  比较大于。

<=? <real1> <real2>

  比较小于等于。

>=? <real1> <real2>

  比较大于等于。

zero? <number>

  判断参数是否为零值。

positive? <real>

  判断参数是否为正数。

negative? <real>

  判断参数是否为负数。

odd? <real>

  判断参数是否为奇数。

even? <real>

  判断参数是否为偶数。

max <real1> <real2>

  计算参数中的最大值。

min <real1> <real2>

  计算参数中的最小值。

add1 <number>

  计算参数加 1 的值。

sub1 <number>

  计算参数减 1 的值。

+ <number1> <number2>

  加法:计算参数的和。

- <number1> <number2>

  减法:计算参数的差。

* <number1> <number2>

  乘法:计算参数的积。

/ <number1> <number2>

  除法:计算参数的商。

abs <real>

  计算参数的绝对值。

  abs幂等操作

注释 同 [RnRS] 和 [RnRK] ,当前仅支持 <real> ,尽管数学上这对 <complex> 也有意义。

floor/ <integer1> <integer2>

  数论除法:计算参数向下取整的整除的商和余数。

floor-quotient <integer1> <integer2>

  数论除法:计算参数向下取整的整除的商。

floor-remainder <integer1> <integer2>

  数论除法:计算参数向下取整的整除的余数。

truncate/ <integer1> <integer2>

  数论除法:计算参数截断取整的整除的商和余数。

truncate-quotient <integer1> <integer2>

  数论除法:计算参数截断取整的整除的商。

truncate-remainder <integer1> <integer2>

  数论除法:计算参数截断取整的整除的余数。

inexact <number>

  取和参数值最接近的不精确数:

  • 若参数是不精确数,则结果是参数值。
  • 否则,若参数超过任意不精确数内部表示的有限值的范围,则结果是未指定宿主类型的对应符号的无限大值。
  • 否则,若不存在和参数数值相等(以 =? 判断)的不精确数,则引起错误。
  • 否则,结果是和参数数值相等的不精确数。

  inexact 是幂等操作。

原理

  这里明确约定了错误条件,这和 [RnRK] 及 [RnRS] 宽松地允许引起错误(也允许不引起要求诊断的错误)不同。

  允许返回无限大值使程序容易判断非预期值的原因。使用浮点数作为不精确数,无限大值的结果符合 [IEC 60559] 的定义。实际 [RnRS] 实现及 klisp 普遍使用这种实现。

  对其它情形保证 =? 比较的后置条件允许用户定义严格相等的转换函数(通过对结果应用 infinite? 可发现并排除无限大值)。

  典型实现中,当参数是 fixnum值时,结果通常不超过不精确数能表示的范围,也不需要引起错误。

注释 同 [R6RS] 和 [R7RS] 的同名过程,及 [R5RS] 的 exact->inexact ;因为当前实现不支持不是 <real><number> ,也同 [RnRK] 的 real->inexact 。关于 [RnRS] 中的命名,另见 R7RS ticket 328 和 [R7RS] 的相关注释。

string->number <string>

  转换字符串为数值。

  若转换成功,结果是对应参数作为外部表示的数值。否则,结果是 #f

  不因转换失败引起错误。

  数值包括所有实现支持的值。

注释 同 [R7RS] 的同名过程,但不支持附加的可选的进位制参数。

number->string <number>

  转换数值为字符串。

注释 同 [R7RS] 的同名过程,但不支持附加的可选的进位制参数且不因特定的参数值出错。

除法约定

  二元除法或者取余数的操作中,第一个参数是被除数,第二个参数是除数。

  数论除法的结果中的数值具有整数类型。

  当除数是不精确数 0 时:

  • 若被除数是非零有限数值或无限大值,则商的符号同被除数的符号。
  • 否则,商的符号未指定。

  当被除数是不精确数时,若除数是精确数 0 ,则结果除符号外同除数是不精确数 0 的情形。

  同时计算商和余数的操作的结果是商和余数构成的列表。

原理

  Scheme 方言及实现中普遍存在不同的除零错误的条件。

  [R5RS] 的过程 / 除以零的条件没有被明确约定。

  [R6RS] 则明确要求过程 / 中:

  • 在所有参数为精确数,若除数存在零值时,引发条件类型为 &assertion 的异常。
  • 否则,以例子指定除数存在零值时的结果:
    • 若被除数为非零有限数值,结果为符号同被除数的。
    • 否则,结果为正的 NaN 值。

  [R7RS] 中修订 [R5RS] 的过程 / 中存在精确数 0 作为除数的除法结果是错误,但这不要求引起错误。实现可以引起错误,或结果具有未指定的错误的值。

  [R7RS] 实际维持 [R5RS] 的条件不变,而非 [R6RS] 附加更多的要求;参见 R7RS ticket 367 ,但其中关于 [R6RS] 实现可转换操作数为不精确数和其它操作一致有误,因为 [R6RS] 的过程 = 不能以此方式实现(参见以上关于比较操作的原理的讨论)。

  不同的 Scheme 实现对零值的处理(包括除零错误的条件)另见这里

  [RnRK] 的合并子 / ,存在任意除数为零值则引起错误。

  因为 [RnRK] 的不精确数是可选模块,不讨论除数的零值是否精确值而保持简单性是合理的。

  [RnRS] 明确要求支持精确数和不精确数(存在不同的字面量),但是只有 [R6RS] 要求存在不精确值时的确切结果。

  NPLAMath的数值操作约定和 [R7RS] 类似,但在此附加和 [R6RS] 类似的要求而覆盖数值操作约定,因为:

  • 不精确数 0 往往来自表示下溢的计算结果,而不是精确的真值 0 ,因此数学上商应存在定义。
  • 同 [RnRS] ,NPLAMath 支持的不精确数允许自然地定义除数为不精确数 0 时的结果:
    • NPLAMath 明确要求支持不精确数(而非 [RnRK] 作为可选的特性提供),能区分不精确数 0 和精确数 0 的不同结果。
    • NPLAMath 的不精确数 0 允许支持符号据此确定作为商的无限值的符号(而非 [RnRK] 无法区分符号而无法按数学定义确定极限值的符号)。
  • 这些规则允许更优化的实现:
  • 不存在 [RnRS] 需要关心继续使用 [R6RS] 规则的一些问题
    • 在被除数和除数都存在零值时,没有同 [R6RS] 的 / 的例子要求结果的符号,逻辑上仍然能保持一致。
    • NPLA1 不需要考虑不完全支持不精确数的(不满足 [RnRS] 符合性)的实现的兼容性等问题。

注释

  同 [R7RS] ,若被除数是零值,且除数不是精确数 0 ,计算结果可能是精确数 0 。但当前实现没有提供精确数替换计算结果中的这种机制。

  NPLA1 std.math 中名称以 floortruncate* 起始的函数的语义同 [R7RS] 的定义,要求参数是整数。一些 Scheme 实现不严格要求整数。

  [R6RS] 的整除基本操作(过程 divmoddiv0mod0 )没有严格定义,仅通过例子说明一些差异,但明确不要求整数。

  [RnRK] 的整除同 [R6RS] ,但描述和 [R6RS] 矛盾;klisp 实现行为同 [R6RS] 。

  更一般的定义参见 [SRFI-141] 和其它相关参考文献:

  • http://dl.acm.org/citation.cfm?id=128862
  • http://people.csail.mit.edu/riastradh/tmp/division.txt
  • https://www.gnu.org/software/guile/manual/html_node/Arithmetic.html
  • https://github.com/johnwcowan/r7rs-work/blob/master/WG1Ballot3Results.md#185-add-sixth-centered-division-operator
  • https://wiki.call-cc.org/eggref/5/srfi-141

字符串库

  提供字符串和正则表达式的相关操作。

  默认加载为根环境下的 std.strings 环境。

模块约定:

  本节约定以下求值得到的操作数:

  • <regex> :正则表达式。

  为提供宿主语言互操作支持,正则表达式以 std::regex 类型表示,实现保证可通过 <string> 对应的 string 值初始化。

  除非另行指定,以上所有正则表达式的操作使用 [ISO C++11] 指定的默认选项,即:

  • std::regex_constants::ECMAScript
  • std::regex_constants::match_default
  • std::regex_constants::format_default

操作:

string? <object>

  <string>类型谓词

++ <string>...

  字符串串接。

string-empty? <string>

  判断字符串是否为空。

注释[SRFI-152] 的过程 string-null?

string<- <string1> <string2>

  字符串赋值

  以第二参数为源,修改第一参数指定的目标。

string-trim <string1>

  删除字符串中指定的连续前缀空白符。

  空白符是 C++ 字符串中 " \n\r\t\v" 的字符之一。

注释 同 [SRFI-152] 的过程 string-trim-both ,但不支持可选参数,且默认指定的空白符不同。

string-trim-left <string1>

  删除字符串中指定的连续后缀空白符。

  空白符同 string-trim 中的定义。

注释 同 [SRFI-152] 的过程 string-trim ,但不支持可选参数,且默认指定的空白符不同。

string-trim-right <string1>

  删除字符串中指定的连续前缀和后缀空白符。

  空白符同 string-trim 中的定义。

注释 同 [SRFI-152] 的同名过程,但不支持可选参数,且默认指定的空白符不同。

string-prefix? <string1> <string2>

  判断第一参数是否包含第二参数作为前缀子串。

注释 同 [SRFI-152] 的同名过程,但不支持可选参数。

string-suffix? <string1> <string2>

  判断第一参数是否包含第二参数作为后缀子串。

注释 同 [SRFI-152] 的同名过程,但不支持可选参数。

string-contains? <string1> <string2>

  判断第一参数是否包含第二参数作为子串。

注释

  一个串总是包含相等的串。空串被任何串包含,且总是不包含任何非空串。

  同 [SRFI-152] 的过程 string-contains ,但不支持可选参数,且结果是 #t#f

string-contains-ci? <string1> <string2>

  判断第一参数是否包含第二参数作为子串,忽略大小写。

  只在单字节字符集内的字符中区分大小写。

注释 除不区分大小写外同 string-contains?

string-split <string1> <string2>

  取第二参数分隔第一参数得到的字符串的列表。

注释 同 [SRFI-152] 的同名过程,但不支持可选参数。

string->symbol <string>

  转换字符串为符号。

symbol->string <symbol>

  转换符号为字符串。

  不检查值是否符合符号要求。

string->regex <string>

  转换字符串为以这个字符串作为串的正则表达式。

  若正则表达式无效,则引起错误

regex-match? <string> <regex>

  判断字符串中是否匹配正则表达式的模式串。

  若 <string> 匹配 <regex> 指定的模式串,结果是 #t ,否则结果是 #f

regex-replace <string1> <regex> <string2>

  替换字符串中的模式串,构造新字符串。

  在 <string1> 的副本中搜索正则表达式指定的模式串的所有匹配,替换为 <string2> 指定的格式字符串。

  结果是替换后的字符串。

注释

  当前实现不处理实现抛出的 std::regex_error 异常。

输入/输出库

  提供输入/输出操作。

  默认加载为根环境下的 std.io 环境。

模块约定:

  文件系统中创建或移除项的函数的返回值是表示操作是否成功的 <bool> 值。

  除非另行指定:

  • 对创建目录的操作,在若目录已存在,则视为操作失败且无作用;否则,若创建失败,则引起错误
  • 对创建多个目录的操作,仅在所有目录都创建成功时操作成功。操作失败可能仍存在部分目录被创建成功。
  • 对访问文件的操作,若读或写失败,则引起错误。
  • 对访问文件系统路径的操作,除非另行指定,若实现环境支持跟随文件系统对象的链接,隐含跟随链接(即解析指定链接的文件系统路径到最终的非链接对象作为路径指定的资源)。
    • 解析链接可能引起错误。
    • 注释 文件系统对象链接的实例如 POSIX 符号链接(symbolic link) 和 Windows 重解析点(reparse point) 。

原理

  文件系统解析文件名可支持链接,但不是所有环境有同等支持。

  Scsh 的 file-not-readable? 等明确不支持 chase? 指定跟随链接,理由是不检查符号链接权限,但这并不充分。

  事实上:

  因此,一般的 API 应当允许区分访问文件是否跟随链接。

注释

  当前派生实现依赖可用的 std.strings环境。

  当前实现的文件系统操作失败可能修改宿主环境的 errno

操作:

file-exists? <string>

  判断参数指定的文件名对应的文件是否存在。

注释

  同:

file-directory? <string>

  判断参数指定的文件名对应的文件是否存在且为目录文件。

注释

  同:

file-regular? <string>

  判断参数指定的文件名对应的文件是否存在且为常规文件。

注释

  同:

readable-file? <string>

  判断参数指定的文件名对应的文件是否存在且可读。

注释

  同:

readable-nonempty-file? <string>

  判断参数指定的文件名对应的文件是否存在、可读且文件内容非空。

writable-file? <string>

  判断参数指定的文件名对应的文件是否存在且可写。

注释

  同:

() newline

  输出换行并刷新缓冲。

  若无法输出,则没有作用。

put <string>

  输出字符串。

puts <string>

  输出字符串和换行并刷新缓冲。

注释putnewline 的组合。

load <string>

  加载参数指定的翻译单元作为源的模块

  加载时在当前环境读取翻译单元后求值,以求值后的这个环境对象作为调用的结果。

  当前实现中,参数为文件系统路径。

  被加载的翻译单元视为对象的外部表示,经读取翻译为 NPLA1 对象。

原理

  和 [R7RS] 不同,load 不支持指定环境,而总是使用当前环境。

  类似 Kernel ,当前环境可通过不同机制改变,而不需由 load 提供特设的支持。例如,可使用 eval指定蕴含 load 的调用的求值使用的环境。

  和其它一些语言的类似命名的功能(如 Lua 的 loadfile )不同,load 的语义隐含从外部来源取得求值构造(evaluation construct) 后在当前环境求值,其中的求值明确允许隐含副作用。在此,load 的求值被视为初始化加载的模块过程中的一部分。

  因为当前不提供取得求值构造的读取(read) 等函数,不要求 load 具有非本机的派生实现。并且,取得求值构造可能有其它方式,如从二进制映像映射(map) 到内部表示等替代,这些实现通常不应被要求为总是具有本机派生实现而降低实现质量。

注释

  参数一般指定视为外部翻译单元的文件名。

  类似 klisp 的同名操作。类似地,不使用 klisp 的 find-required-filename 机制,直接以宿主的运行环境为基准使用路径。

  和 klisp 不同,在尾上下文中求值被加载后读取的对象,并以其求值结果作为操作的结果,且错误不影响操作的结果。

  [Shu09] 缺少 load 的详细描述而仅有标题。

  另见以下关于 get-module 的说明。

get-module <string> <environment>?

  创建标准环境并在其中加载模块。

  创建标准环境并以这个环境为当前环境加载 <string> 指定的翻译单元作为源的模块。

  若第二参数非空,则在加载前首先绑定创建的环境中的 module-parameters 变量为第二参数的值。

  结果在加载完成后取得,是先前被创建的标准环境。

注释

  第一参数的作用同 load 的参数。

  类似 klisp 和 [Shu09] 中的同名操作。

  klisp 文档中的 load 描述中求值环境有误:

  • 按 [Shu09] 一致的描述和 klisp 的实际实现,调用 load 时应在当前环境求值,而不同于 [Shu09] 的 get-module 中描述的使用创建的新标准环境进行求值。
  • 否则,使用 [Shu09] 的 get-module 的派生不能实现 klisp 和 [Shu09] 中描述的 get-module 的预期语义。

absolute-path? <string>

  判断参数是否指定绝对路径。

注释

  同:

path-parent <string>

  参数字符串视为路径,结果是字符串指定的路径所在的父目录(当路径非根目录),或路径自身转换到的字符串值。

  结果的路径不带有结尾分隔符。不检查路径实际存在。

注释

  同:

remove-file <string>

  移除参数指定的路径命名的文件。若成功结果为 #t ,否则结果为 #f

注释

  同:

  • [STklos 的同名过程](https://stklos.net/Doc/html/stklos-ref-4.html#delete-file) ,但通过函数值指定调用失败而非引起错误。
  • [R7RS] 的 delete-file ,但通过函数值指定调用失败而非引起错误。

create-directory <string>

  创建参数指定的名称的文件系统目录。

原理

  create-directory 在一些其它语言的实现中命名为 make-directory

  NPLA1 中,作为非正式约定,make 前缀被预留给通过函数值得到创建的对象的过程。

  这和 Gauche 不区分两者不同。

  使用 create-directory 的命名也和 [SRFI-170] 一致,但 NPLA1 中,目录存在时不引起错误而以函数值 #f 指示。

  此外,[ISO C++] 中存在类似名称的函数 std::filesystem::create_directory

注释

  类似:

create-directory* <string>

  创建参数指定的名称的文件系统目录及其必要的父目录。

  create-directorycreate-directory* 只创建一级目录时的行为确保一致。

注释

  类似:

create-parent-directory* <string>

  创建参数指定的名称对应的父目录及其必要的父目录。

注释

  类似 [Racket] 的 make-parent-directory* ,但不支持非字符串参数和可选的权限参数。

  当前实现中,若创建目录失败而引起错误,抛出 std::system_error 异常。

系统库

  提供和实现环境的交互功能。

  默认加载为根环境下的 std.system 环境。

对象:

version-string

  当前实现的版本字符串。

  类型为 <string>

build-number

  当前实现的构建版本号。

  类型为 <integer> ,值为正整数。

revision-description

  实现的版本说明。

  类型为 <string>

操作:

() get-current-repl

  取表示当前 REPL 环境的引用值。

() cmd-get-args

  返回宿主环境程序接受的命令行参数列表。

  其中参数数组保存在实现内部访问的对象中。

  传递给 REPL 的命令行参数通常是宿主程序中主函数的 argv 参数数组中处理后去除特定参数后的程序。

  宿主程序复制 argv 到这个数组作为副本后,作为返回值的列表的来源。

env-get <string>

  取宿主环境的环境变量字符串。

  字符串参数指定环境变量的名称。

env-set <string1> <string2>

  设置宿主环境的环境变量字符串。

  两个字符串参数分别指定环境变量的名称和设置的值。

  使用 env-getenv-set 及对应宿主环境的操作不保证线程安全。

env-empty? <string>

  判断字符串指定名称的环境变量是否为空。

system <string>

  以 std::system 兼容的方式调用外部命令。

  结果是宿主环境命令返回的命令退出状态。

注释

  类似 Chez Scheme 的外部接口 ,但更接近 [ISO C] 和 [ISO C++] 的原始含义,当前不提供关于信号等依赖特定实现环境的保证。

  使用 [ISO C] 和 [ISO C++] 的宿主环境的命令退出状态是 int。由 NPLA 实现环境,这里的结果的宿主类型是确定的。而 NPLA1 数值使用 NPLA 数学功能确保其被映射确定的数值类型

system-get <string>

  调用命令,返回命令调用结果。

注释 在典型的 C++ 宿主实现中,命令的调用结果是来自标准输出的数据。

  返回一个两个元素的列表,分别是管道输出字符串和宿主环境命令返回的命令退出状态。

  调用命令和 system 的方式类似。

make-temporary-filename <string1> <string2>

  取适合命名新创建的临时文件的随机文件名。

  参数分别指定前缀和后缀。参数可能为空。随机文件名以生成的有限长度的随机字符串添加前缀和后缀组成。

  生成的字符串只包含 C++ 基本字符串的可打印字符,且不使用大写字母,以兼容大小写不敏感的文件系统。

  随机的文件名被打开以检查是否可访问。若失败则重试,若最终失败,则引起错误。

注释

  功能类似 POSIX ::tmpnam::mkstemp ,以及 Guile 对应的过程 tmpnammkstemp ,有以下不同:

  • tmpname 类似,结果是文件名,而不是打开的文件或端口。
  • mkstemp 类似,使用模板字符串作为文件名,但模板不在参数中指定。

   保证生成的文件名在不同宿主实现(文件系统)中的兼容性。

  当前实现中的随机字符串长度为 6 。当前实现中重试的上限是 16 次(宿主平台)或 1 次(非宿主平台)。若最终失败,抛出 std::system_error 异常。

模块管理

  提供使用模块和其中符号的相关功能。

  默认加载为根环境下的 std.modules 环境。

模块约定:

  需求字符串(requirement string) 是具有 <string> 类型的非空字符串。

  若操作的形式参数是需求字符串,实际参数是空字符串,则引起错误

  本模块共享可变管理状态,以支持操作访问确定的模块字符串集合。

  本模块隐含一个字符串序列作为需求字符串模板。

  除非派生实现另行指定,需求字符串模板初始化后不可变。

  需求字符串模板初始化的值由实现定义。

原理

  为避免名称污染等问题,不提供显式指定命名环境的模块创建操作,如 Lua 5.1 的 module 函数存在一些问题

  不提供访问创建的环境的操作,以避免污染外部的访问。若需公开其中的变量绑定,可使用返回或模块参数。

注释

  当前实现中,若环境变量 NPLA1_PATH 的值非空,则需求字符串模板是这个值以单字符子串 ";" 分隔后的结果;否则,默认值是序列 "./?" 和 "./?.txt" 构成的序列。

  当前派生实现依赖可用的 std.stringsstd.iostd.system环境。

  通过不经过本模块的操作(如互操作)、重复字符串模板的重复项、符号链接和字符串大小写不敏感的文件名等可能绕过本模块的注册机制而重复加载同一个外部文件。本模块的操作不对这些情形进行任何检查。

操作:

registered-requirement? <string>

  判断参数是否是已在本模块注册的需求字符串。

register-requirement! <string>

  在本模块注册参数为需求字符串。

  若已被注册,则引起错误;否则,在内部创建标准环境

  结果是创建的环境的弱引用

unregister-requirement! <string>

  在本模块注册解除参数为需求字符串。

find-requirement-filename <string>

  查找需求字符串对应的文件名。

  在需求字符串模板中顺序地搜索字符串。若不存在这样的结果,则引起错误;否则,结果是匹配字符串的搜索结果。

  判断需求字符串模板中的每一个字符串是否能被需求字符串匹配时,首先替换字符串中的单字符子串 "?" 为需求字符串,取得替换结果,再判断它是否为可读的文件的文件名。

  替换字符串时,每一个子串被同时一次替换;不对替换结果进一步递归地替换。

require <string> <environment>?

  按需在新标准环境加载需求字符串对应的模块。

  若第二参数非空,则在加载前首先绑定创建的环境中的 module-parameters 变量为第二参数的值。

  若参数指定的需求字符串没有注册,则注册需求字符串并加载同调用 find-requirement-filename 等价的方式搜索得到的结果,并保存加载的结果;否则不进行加载。

  结果是保存的加载的结果。

注释

  类似 klisp 的 ports 模块中的同名应用子,但有以下不同:

  • 同时保存创建的环境,以避免因程序没有保存环境引用而无效化,使访问变量绑定的程序具有未定义行为。
  • 结果是加载结果(同模块 std.io 中的 load)而不是 #inert
  • 支持同 std.io 中的 get-module 的可选参数。

附录

进一步阅读

  关于 NPL 语言派生实现的具体实现,参见 YSLib 项目文档 doc/NPL.txt

Kernel 实现

  NPL 文法是 S-表达式语法和 Kernel 兼容语义的简化,某些 NPLA1 程序可以原封不动地作为 Kernel 程序运行。因为参考文献可用性以及相似性几乎独一无二,建议深入使用前参照 Kernel 相关的文档并实际使用,以和 NPL 进行比较(当前 NPL 开发文档也引用包括 [RnRK] 在内的一些规格说明)。现有较完整可用的一个 Kernel 实现是 Klisp

计划支持特性

  在 Kernel 中已存在但当前 NPLA1 未支持,而计划在未来添加的特性:

  • 一等续延(first-class continuation)
  • Promises
    • 虽然 NPLA1 之前有部分原生实现,但计划改用基于封装类型派生。
  • Keyed Variables
    • 有效的 keyed dynamic varabile 实现可能依赖一等续延数据结构。
    • 有效的 keyed static varabile 实现需要调整环境数据结构。
  • Ports
    • 需要调查内存映射文件和其它系统支持。
  • Libraries
    • 可能需要提供多种不同的接口。

  已提供但考虑可能优化实现的特性:

  • 封装类型
    • 需要调查互操作相关的特性和持久化支持。

  计划考虑提供其它设计替代选项的 Klisp 特性:

  • Characters
    • 满足互操作要求的前提下,使用设计更合理的类型替代。
  • Vectors
    • 使用更完善的用户自定义派生类型替代。

  其它 NPL 计划实现特性: