Modula-3![]() |
编程范型 | 指令式, 过程式, 结构化, 模块化, 并发 |
---|
语言家族 | Wirth Modula |
---|
設計者 | Luca Cardelli, James Donahue, Lucille Glassman, Mick Jordan, Bill Kalsow, Greg Nelson[1] |
---|
實作者 | DEC, Olivetti, elego Software Solutions GmbH |
---|
发行时间 | 1988年,36年前(1988) |
---|
型態系統 | 强类型, 静态, 安全或在不安全时显式安全隔离 |
---|
作用域 | 词法 |
---|
系统平台 | IA-32, x86-64, PowerPC, SPARC |
---|
操作系统 | 跨平台: FreeBSD, Linux, Darwin, SunOS |
---|
網站 | www.modula3.org |
---|
|
SRC Modula-3, CM3[2], PM3[3], EZM3[4], M3/PC Klagenfurt[5] |
|
ALGOL, Euclid, Mesa, Modula-2, Modula-2+, Oberon, Object Pascal |
|
C#, Java, Nim[6], Python[7], Baby Modula-3[8] |
Modula-3是一种系统编程语言,它是叫做Modula-2+的升级版本的Modula-2的后继者。虽然它已经在研究界有了影响力,受其影响的语言有Java、C#和Python[9],但未能在工业上被广泛采用。它是在1980年代末由在数字设备公司(DEC)系统研究中心(SRC)和Olivetti研究中心(ORC)的Luca Cardelli、James Donahue、Lucille Glassman、Mick Jordan(之前在Olivetti软件技术实验室工作)、Bill Kalsow和Greg Nelson设计。
Modula-3的主要特点,是保持系统编程语言的强力,同时具有简单性和安全性。Modula-3意图延续Pascal的类型安全和Modula-2的模块化编程传统,同时为实际编程引入新构造。特别是Modula-3添加了如下支持:例外处理、关键字参数与缺省参数值、有跟踪的引用与垃圾回收、面向对象、不透明类型及其披露、泛型、多线程和不安全代码显式标记。Modula-3的设计目标,是以非常基本的形式,实现指令式编程语言的大部份重要的现代特征。因此省略了涉嫌危险和复杂的特征,如多重继承和运算符重载。
发展历史
Modula-3项目始于1986年11月,当时莫里斯·威尔克斯向尼克劳斯·维尔特写信,提出了一些关于Modula新版本的想法。威尔克斯在此之前一直在DEC工作,他回到英格兰并加入了Olivetti的研究策略委员会。Wirth已经转移到了Oberon,但Wilkes的团队在Modula名下继续开发没有任何问题。语言定义于1988年8月完成,并于1989年11月更新了版本[10]。DEC和Olivetti的编译器很快就出现了,随后又有了第三方实现。
它的设计受到SRC和Acorn计算机研究中心(ARC,在Olivetti收购Acorn之后叫做ORC)在Modula-2+语言上的工作的很大影响[11],Modula-2+是编写DEC Firefly多处理器VAX工作站的操作系统的语言[12][13],还是在基于ARM的Acorn Archimedes系列计算机的ARX操作系统项目中,ARC编写用于Acorn C和模块执行库(CAMEL)的Acorn编译器的语言[14]。正如修订的Modula-3报告所述[14],该语言演化自Mesa、Modula-2、Cedar和Modula-2+,还受到其他语言的影响如:Object Pascal,Oberon和Euclid。
在20世纪90年代,Modula-3作为一种教学语言获得了可观的传播[15],华盛顿大学在1994年发行的微内核操作系统SPIN是用Modula-3开发的[16],但它从未广泛用于工业用途。造成这种情况的原因,可能是Modula-3的关键支持者DEC的消亡,特别是在1998年DEC被出售给康柏之前就不再有效地维护它了。无论如何,尽管Modula-3有着简单性和强大功能,似乎当时对有限实现了面向对象编程的过程式编译语言的需求很少。
Critical Mass公司曾在一段时间内提供了一个名为CM3的商业编译器,它由以前DEC SRC的在DEC被出售给Compaq之前雇用的一个主要实现者维护,和叫做Reactor的一个集成开发环境以及可扩展的Java虚拟机(以二进制和源格式许可发行并可用Reactor来构建)。但该公司在2000年停止了活动,并将其产品的一些源代码提供给了elego软件解决方案有限公司。
基本上,Modula-3的唯一企业支持者是elego软件解决方案有限公司,它继承了Critical Mass的资源,并且已经以源代码和二进制形式发布了几个版本的CM3系统。Reactor IDE在几年后才开源发布,新的名称是CM3-IDE。2002年3月,elego还接管了此前由蒙特利尔工程学院维护的另一个活跃的Modula-3发行版PM3的存储库,在此后几年间以HM3名义进行了持续改进,直至最终被淘汰。
Modula-3是文档记录了语言特征演化的少见语言之一,在《用Modula-3系统编程》中[17],收录了设计者对四个设计要点的讨论:结构等价与名称等价、子类型规则、泛型模块和参数模态。Modula-3现在主要是在大学中的比较编程语言课程中教授,其教科书已绝版。
程序示例
Modula-3中所有的程序至少具有一个模块文件,而大多数程序还包括一个接口文件,客户端使用它来访问来自模块的数据。语言语法的一个常见示例是Hello world程序:
MODULE Main;
IMPORT IO;
BEGIN
IO.Put("Hello World\n")
END Main.
与其他语言一样,Modula-3程序必须有导出Main
接口的实现模块,如上述示例中实现模块缺省导出了同名的接口,它可以处在名为Main.m3的文件中;或者是通过EXPORTS
导出Main
接口的实现模块,比如:
MODULE HelloWorld EXPORTS Main;
IMPORT IO;
BEGIN
IO.Put("Hello World\n")
END HelloWorld.
建议这个模块所在的文件的名字与实际模块名字相同,当它们不同时编译器只是发出一个警告。
语言设计理念
Modula-3委员会将Modula-3设计为ALGOL语言家族的现代代表,他们认为:
- 始自BCPL的C语言家族,其优美在于以适度的代价得到了相对汇编语言的巨大飞跃,但它们接近于目标机器的低层编程模型是天然有危险性的;其中的衍生语言C++,通过增加对象丰富了C,但它放弃了C的最佳优点简单性,却没有消除它最差缺点即低层编程模型。
- 另一个极端是LISP语言家族,它们有着混合了lambda演算和配对函数理论的编程模型,但它们趋于难以高效实现,因为在编程模型中值的统一对待,招致了所有的值都由指针统一表示的运行时间系统;这个家族的语言的良好实现避免了多数代价而能用于系统编程,但它们普遍秉性仍旧向往堆分配而非栈分配,这种运行时间系统经常将它们孤立于封闭环境中,而不能容纳用其他语言写的程序。
- ALGOL语言家族处在这两个极端之间,它的现代代表包括Pascal、Ada、Modula-2等,这些语言拥有的编程模型反映了随机存取机在工程上的约束,隐藏了任何特定于机器的细节。它们放弃了LISP家族的优美和数学对称性,使得无须特殊技巧就可能高效的实现,并且避免了C家族的多数危险和依赖机器的特征。
ALGOL家族语言都有强类型系统,其基本想法是把值空间划分成类型,限制变量持有单一类型的值,并且限制运算应用于固定类型的运算数。这个语言家族的趋势,在1960年代是朝向控制流程和数据结构特征,在1970年代是朝向信息隐藏特征如接口、不透明类型和泛型,在1980年代是采纳来自LISP家族和始自BCPL的C家族的仔细挑选的技术。
Modula-3委员会坚持一个自称为“复杂度预算”的设计原则:在五十页内将语言准确而易读的描述出来,为此只容纳最有用的特征。精选出来的特征直接针对两个主要目标:
这些特征中任何一个都没有特殊的新颖性,但是组合起来是简单而强力的。人们越是更好的理解程序,就越是使用更大的建造块去构造它。指令之后是语句,语句之后是过程,过程之后是接口,下一步应当是抽象类型。在理论层面,抽象类型,是通过其操作的规定,而非通过其数据的表示,来定义的类型。
词法记号
关键字:
AND
ANY
ARRAY
AS
BEGIN
BITS
BRANDED
BY
CASE
CONST
DIV
|
DO
ELSE
ELSIF
END
EVAL
EXCEPT
EXCEPTION
EXIT
EXPORTS
FINALLY
FOR
|
FROM
GENERIC
IF
IMPORT
IN
INTERFACE
LOCK
LOOP
METHODS
MOD
MODULE
|
NOT
OBJECT
OF
OR
OVERRIDES
PROCEDURE
RAISE
RAISES
READONLY
RECORD
REF
|
REPEAT
RETURN
REVEAL
ROOT
SET
THEN
TO
TRY
TYPE
TYPECASE
UNSAFE
|
UNTIL
UNTRACED
VALUE
VAR
WHILE
WITH
|
保留标识符:
ABS
ADDRESS
ADR
ADRSIZE
BITSIZE
BOOLEAN
|
BYTESIZE
CARDINAL
CEILING
CHAR
DEC
DISPOSE
|
EXTENDED
FALSE
FIRST
FLOAT
FLOOR
INC
|
INTEGER
ISTYPE
LAST
LONGINT
LONGREAL
LOOPHOLE
|
MAX
MIN
MUTEX
NARROW
NEW
NIL
|
NULL
NUMBER
ORD
REAL
REFANY
ROUND
|
SUBARRAY
TEXT
TRUE
TRUNC
TYPECODE
VAL
|
运算符:
+ |
< |
#
|
= |
; |
.. |
:
|
- |
> |
{ |
} |
| |
:= |
<:
|
* |
<= |
( |
) |
^ |
, |
=>
|
/ |
>= |
[ |
] |
. |
& |
|
此外,注释是开于(*
并合于*)
的任意字符的序列。注释可以嵌套并可以扩展跨越多于一行。
基础性语法和语义
Modula-3承袭了ALGOL家族传统的强类型,并且为了使类型系统尽可能的统一,采用了如下原则:
- 首先,类型或目标类型确定是没有歧义的,所有的表达式的类型由它的子表达式来确定,而非由它的使用来确定。
- 其次,没有自动转换。
- 第三,可赋值性和类型兼容性,是依据单一的在语法上用算符
<:
指定的子类型关系来定义的,它具有如下性质:如果T
是U
的子类型,则所有的T
的成员都是U
的成员,子类型关系是自反和传递的;子类型关系是处理具有继承的对象所需要的,但也能用于确定常规类型的兼容性规则。
一个典型的不安全运行时间错误,是释放仍可由活跃引用(悬空指针)触及的数据结构。避免这个问题的唯一可靠方式,是不再触及存储的自动释放,或称为垃圾收集。Modula-3因而提供了有跟踪的引用,它类似于Modula-2的指针,除了它们所指向的存储被保存在有跟踪的堆之中之外,在这里它们将在到它们的引用都消失时自动释放。垃圾收集的另一个巨大利益是简化了接口,没有垃圾收集,接口就必须规定客户或实现是否有责任释放每个分配的引用,和在什么条件下可以安全的去这么做。
例外是一次退出多层作用域的控制结构。一个例外被重复的发起退出当前活跃的作用域,直到找到给这个例外的处理器(handler),并将控制权移交给这个处理器,如果没有处理器,则计算以某种依赖于系统的方式终止,比如进入调试器。在Modula-3中,EXIT
和RETURN
语句的语义,分别使用退出例外exit-exception
和返回例外return-exception
来定义。Modula-3的设计者认为语言实现,除了这种返回例外和退出例外之外,更应当以各种例外为代价,加速正常的结果产出;为每个引发的例外花费一千条指令,来为每个过程节约一条指令都是合理的。
块与声明
声明介入一个常量、类型、变量、例外或过程的一个名字。这个名字的作用域是包含这个声明的块。一个块可以出现为一个模块或过程的本体,或一个块语句,Pascal和Modula-2没有块语句[18]。一个块有如下形式:
这里的Decls
是成序列的声明,而S
是一个语句,经常是顺序复合语句即成序列的语句,它是这个块的执行部份。这里的Decls
与S
在缩进上对齐,是因为在Pascal语言家族中,可能含有嵌套过程声明的声明序列的位置相较于ALGOL 60,从紧随在关键字BEGIN
之后转移到了紧靠在其前面。一个块的声明可以介入一个名字最多一次,然而一个名字可以在嵌套的块中重新声明。在块中这些声明的次序,除了确定变量的初始化次序之外无关紧要。
一个Modula-3程序规定了一个计算,它作用在一序列的叫做地点(location)的数字元件之上。变量是地点的一个集合,它依据这个变量的类型所确定的约定,表示一个数学上的值。如果一个值可以由类型T
的某个变量来表示,则称谓这个值是T
的一个成员,并且T
包含这个值。
声明为一个变量、类型、过程、常量或例外指定一个被称为标识符的符号作为名字。一个声明应用于其上的程序区域,叫做这个声明的作用域。作用域是可以嵌套的。一个标识符的含义,由这个标识符在其中声明的最小的包围作用域来确定。
类型声明有如下形式:TYPE T = U
,这里的T
是一个标识符,而U
是一个类型或类型表达式。在需要类型的各种地方,通常允许类型表达式。
变量声明有如下形式:VAR id: T := E
,这里的id
是一个标识符,T
是除了开放数组之外的一个非空类型,而E
是一个表达式。它声明了id
是类型T
的一个变量,其初始值是E
的值。:= E
和: T
任何一个都可省略,但二者不能都省略。如果省略了T
,它采用E
的类型。如果省略了E
,初始值是类型T
的任意值。如果二者都存在,E
必须可赋值给T
。初始值是一种简写,等价于在随后的块的可执行部份开始处插入赋值id := E
,如果多个变量有初始值,按它们声明的次序插入它们的赋值。形式VAR v_1, ..., v_n: T := E
,是VAR v_1: T := E; ...; VAR v_n: T := E
的简写。
常量声明有如下形式:CONST id: T = C
,这里的id
是一个标识符,T
是一个类型,而C
是一个常量表达式。它声明id
为具有类型T
和值C
的一个常量。: T
可以省略,在这种情况下id
的类型是C
的类型。如果T
存在,则它必须包含C
。
值与类型
Modula-3的类型系统使用结构等价,而非Modula-2的名称等价即类型构造子的每次出现产生一个新类型。在结构等价中,在如果两个类型的定义在展开的时候,也就是在所有常量表达式都被替换为它们的值,并且所有类型名字都被替代为它们的定义的时候,变成相同的,则这两个类型是相同的。在递归数据类型的情况下,这个展开是部份展开的无穷极限。
有三种序数类型:枚举、子范围和整数类型,预定义了如下序数类型:INTEGER
、LONGINT
、CARDINAL
(如同子范围[0..LAST(INTEGER)]
)、BOOLEAN
(枚举{FALSE, TRUE}
)和CHAR
(包含至少256个元素表示ISO-Latin-1代码的枚举)。整数和枚举元素合起来叫做序数值。序数值v
如果是整数或扩展精度整数,则v
的基础类型分别是INTEGER
或LONGINT
,否则v
的基础类型是包含它的那个唯一性的枚举类型。两个序数如果有相同的值则二者相等。
有三种浮点类型:REAL
、LONGREAL
和EXTENDED
,这些类型的属性规定在对应的必要接口中。两个浮点数如果定义它们的底层实现相同则二者相等。
一个引用值要么是NIL
,要么是一个叫做所引用者(referent)的变量的地址。预定义了如下引用类型:REFANY
,它包含所有的有跟踪的引用,ADDRESS
,它包含所有的无跟踪的引用,和NULL
,它只包含NIL
。Modula-3支持在运行时间的数据分配。分配操作有如下形式:NEW(T, ...)
,这里的T
是除了预定义的REFANY
、ADDRESS
或NULL
之外的引用类型。两个引用如果寻址相同的地点则二者相等。
记录是叫做这个记录的字段的命名变量的一个序列,记录类型的声明有如下形式:TYPE T = RECORD FieldList END
,在这里的类型表达式中,算子RECORD
是类型构造子,而它的实际参数是字段声明的一个列表FieldList
,其中每个字段都有如下形式:fieldName: Type := default
,这里的字段名字fieldName
是一个标识符,而字段类型Type
是除了开放数组之外的非空类型,而字段缺省值default
是一个常量表达式。形式f_1, ..., f_m: Type := default
,是f_1: Type := default; ...; f_m: Type := default
的简写。两个记录如果有相同的字段,并且对应的字段都相等则二者相等。
在Modula-3中的对象,非常类似于Simula 67中的对象:它们总是引用,它们拥有数据字段和方法二者,并且它们有着单一继承而非多重继承。预定义了如下对象类型:ROOT
,它是没有字段或方法的有跟踪的对象类型,和UNTRACED ROOT
,它是没有字段或方法的无跟踪的对象类型。
不透明类型是一个名字,它指示某个给定引用类型的未知子类型。预定义了两个不透明类型:TEXT <: REFANY
和MUTEX <: ROOT
,它们分别表示文本串和互斥信号量,它们的属性分别规定于必要接口Text
和Thread
之中。
数组是叫做这个数组的元素的组成变量的有索引的搜集。有两种数组类型,固定的和开放的。固定数组的长度是在编译时间确定的,开放数组的长度是在运行时间分配确定的,这个长度此后不能改变。两个数组如果有相同的长度,并且对应的元素都相等则二者相等。
包装类型的变量出现在记录、对象或数组中,占据指定数量的位元,并毗邻于前导的字段或元素紧压打包。包装类型的声明有如下形式:TYPE T = BITS n FOR Base
,这里的Base
是一个基础类型,而n
是整数取值的常量表达式。
集合是取自某个序数类型的值的搜集。集合类型声明有如下形式:TYPE T = SET OF Base
,这里的基础类型Base
是一个序数类型。两个集合如果有相同的元素则二者相等。
不包含值的一个类型是空的,例如[1..0]
是一个空类型。空类型可以用来建造非空类型,比如SET OF [1..0]
。声明一个空类型的一个变量是静态错误。
所有的表达式都有一个唯一的类型,但是一个值可以是很多类型的一个成员,例如值6
是[0..9]
和INTEGER
二者的成员,因此论述“一个值的类型”将会是有歧义的。短语“x
的类型”,意味着“表达式x
的类型”;然而短语“x
是T
的成员”,意味着“x
的值是T
的成员”。但在有一种情况下,一个值可以称为有着一个类型:所有的对象或有跟踪的引用值,包括了一个类型代码,它叫做这个引用值的分配类型。分配类型通过TYPECASE
语句来测试。
语言规定提供了类型运算BITSIZE()
、BYTESIZE()
和ADRSIZE()
,分别返回变量x
或者类型T
的那些变量的位元数目、8位字节的数目和可寻址地点的数目。在所有情况下,x
必须是指定式,而T
必须不是开放数组类型,指定式x
只在它的类型是开放数组的时候求值。
序数类型
枚举类型声明有如下形式:TYPE T = {id_1, id_2, ..., id_n}
,这里的id
是各不相同的标识符,类型T
是n
个值的有序集合,表达式T.id_i
指示这个类型在递增次序下的第i
个值,空枚举{ }
是允许的。
子范围类型声明有如下形式:TYPE T = [Lo..Hi]
,这里的Lo
和Hi
是有相同的基础类型的两个序数值,T
的值是从Lo
到Hi
含二者所有的值,它们必须是常量表达式,如果Lo
超过Hi
,则子范围是空的。
每个不同的枚举类型介入新的一组值,而子范围类型重复使用底层类型的值。例如:
TYPE
T1 = {A, B, C};
T2 = {A, B, C};
U1 = [T1.A..T1.C];
U2 = [T1.A..T2.C]; (* sic *)
V = {A, B}
这里的T1
和T2
是相同类型,因为它们有相同的展开定义。U1
和U2
也是相同的类型,因为T1.C = T2.C
。类型T1
和U1
却是不同的,尽管它们包含相同的值,但是T1
的展开定义是枚举,而U1
的展开定义是子范围。类型V
是第三个类型,它的值V.A
和V.B
无关于值T1.A
和T1.B
。
语言规定提供了支持序数类型的一些类型运算:对于序数类型T
,NUMBER(T)
返回在T
中元素的数目,FIRST(T)
返回T
的最小值,LAST(T)
返回T
的最大值。ORD(T.id)
将一个枚举的元素T.id
,转换成表示它在枚举次序中所处位置的整数,如果元素的类型是枚举T
的子范围,结果是元素在T
内的位置,而非在子范围内。VAL(i, T)
将表示在枚举类型T
中位置的整数i
,转换成在这个枚举中此位置上的那个元素,如果T
是枚举类型的子范围,结果是在源出枚举类型的位置i
上的元素,而非在子范围内。如果n
是类型T
的一个整数,则有平凡的:ORD(n) = VAL(n, T) = n
。
数组类型
固定数组类型声明有如下形式:TYPE T = ARRAY Index OF Element
,这里的索引类型Index
是序数类型,而元素类型Element
是除了开放数组之外的任何类型。开放数组类型声明有如下形式:TYPE T = ARRAY OF Element
,这里的元素类型Element
是任何类型。
数组类型T
的值,是元素类型为Element
的数组;对于固定数组类型,这个数组的长度是索引类型Index
的元素数目;对于开放数组类型,这个数组的长度是任意的;开放数组的索引类型是INTEGER
子范围[0..n-1]
,这里的n
是这个数组的长度。开放数组类型只能用作形式参数的类型,引用类型的所引用者,另一个开放数组的元素类型,或作为数组构造子中的类型。
多维数组的形状是它每一维的长度的序列。更精确的说,一个数组的形状是它的长度跟随着任何它的元素的形状,非数组的形状是空序列。数组如果有相同的类型和形状,则它们是可赋值的。如果赋值的要么来源要么目标是开放数组,则需要运行时间检查。形如ARRAY Index_1, ..., Index_n OF Element
的表达式,是ARRAY Index_1 OF ... OF ARRAY Index_n OF Element
的简写。
如果a
有数组类型T
,则a[i]
指示a
的一个指定元素,它在数组中的位置对应于i
在索引类型中位置。形如a[i_1, ..., i_n]
的表达式,是a[i_1]...[i_n]
的简写。索引的解释由一个数组的类型确定,而非它的值;数组赋值改变数组变量的值,而非它的类型。例如:
VAR
a := ARRAY [1..3] OF REAL {1.0, 2.0, 3.0};
b: ARRAY [-1..1] OF REAL := a;
这里在a
或b
做元素值变更之前,a = b
为TRUE
,尽管a[1] = 1.0
而b[1] = 3.0
。
可以使用NUMBER(A)
,得到一个固定数组类型的索引类型中的元素数目,使用FIRST(A)
和LAST(A)
,分别得到一个固定数组类型的索引类型的最小元素和最大元素。可以使用NUMBER(a)
,得到一个数组的索引类型中的元素数目,使用FIRST(a)
和LAST(a)
,分别得到一个数组的索引类型的最小元素和最大元素,在这种情况下,表达式a
只在它指示一个开放数组的时候求值。
引用类型
一个引用类型要么是有跟踪的要么是无跟踪的。当到已分配的一段存储的所有的有跟踪的引用都消失时,系统对这段存储进行垃圾回收。有跟踪的引用类型以如下形式来声明:TYPE T = REF Type
,这里的Type
是任何类型。T
的值是到类型Type
的变量的有跟踪的引用,Type
叫做T
的所引用类型。无跟踪的引用类型以如下形式来声明:TYPE T = UNTRACED REF Type
,这里的Type
是任何无跟踪的类型(这个限制在不安全模块中解除)。
两个引用类型如果都是有跟踪的或都是无跟踪的,那么它们有相同的引用类。一个一般性的类型是有跟踪类型的条件为,它是有跟踪的引用类型,其任何字段类型是有跟踪类型的记录类型,其元素类型是有跟踪类型的数组类型,或其底层未包装类型是有跟踪类型的包装类型。
在有跟踪的和无跟踪的二者情况下,关键字REF
可选的可以前导上BRANDED b
,这里的b
是叫做铭牌(brand)的文本常量。铭牌用来区分原本上相同的类型,它们没有其他语义效果。在一个程序中所有铭牌都必须是唯一性的。如果BRANDED
出现而b
缺席,系统自动的为b
提供一个唯一的值。
分配操作NEW(T, ...)
返回T
的所引用类型的一个新分配的变量的地址,如果T
是对象类型,则返回到有配对的方法套件的一个新分配的数据记录的引用。NEW()
返回的引用不同于所有的现存引用。这个新引用的分配类型是T
。如果T
的所引用类型为空则是一个静态错误。如果T
是到有k
个开放维度的数组的引用,NEW(T)
操作有如下形式:NEW(T, n_1, ..., n_k)
,这里的这些n
是指定新数组在它的前k
个维度上的长度的整数取值表达式。
所有的对象类型和有跟踪的引用类型(包括NULL
)都有一个关联的整数代码。不同的类型有不同的代码。给一个类型的代码对程序的任何单一执行都是常量,但对不同的执行可以不同。类型运算TYPECODE(T)
,返回类型T
的这个代码,而TYPECODE(r)
,返回引用r
的分配类型的这个代码。如果T
是REFANY
,或者不是对象类型或有跟踪的引用类型,则是一个静态错误。类型运算ISTYPE(x, T)
,测试x
是否为T
的成员,这里的T
必须是对象类型或有跟踪的引用类型,而x
必须可赋值给T
。
表达式
表达式规定产生一个值或变量的一个计算。所有的表达式都有一个静态确定的类型,它包含这个表达式可以产生的所有的值。在语法上,一个表达式要么是一个运算数,要么是应用于自身也是表达式的实际参数的一个运算。运算数是标识符、文字或类型。通过递归的求值一个运算的实际参数并应用这个运算,来求值一个表达式。实际参数的求值次序,对除了短路求值的AND
和OR
之外的所有运算都是未定义的。
对一个过程的调用,在这个过程是返回一个结果的函数过程的时候,是称为函数应用的一个表达式,这个表达式的类型是过程的结果类型。
Modula-3语言规定提供了前缀算术运算:+
、-
,中缀算术运算:+
、-
、*
、/
、DIV
、MOD
,数值函数运算:ABS(x)
、FLOAT(x, T)
、FLOOR(x)
、CEILING(x)
、ROUND(r)
、TRUNC(r)
、MAX(x, y)
、MIN(x, y)
,关系运算:=
、#
、 <=
、>=
、>
、<
,逻辑运算:NOT p
、p AND q
、p OR q
,文本串接运算:a & b
,内存分配运算:NEW(T, ...)
,集合运算:并集+
、差集-
、交集*
、对称差集/
和隶属关系测试e IN s
,它在序数e
是集合s
的元素的时候返回TRUE
。
表达式可建构自如下文字:整数文字、实数文字、字符文字和文本文字。集合、数组和记录有构造子表达式:
S{e_1, ..., e_n}
,这里的S
是一个集合类型,而这些e
是表达式或lo..hi
形式的范围。这个构造子指示类型为S
的一个值,它包含列出的那些值和在列出的范围中的那些值,这些e
、lo
和hi
必须可赋值给类型S
的元素类型。
A{e_1, ..., e_n}
,这里的A
是一个数组类型,而这些e
是表达式。这个构造子指示类型为A
的一个值,它包含遵循列出次序的这些列出元素,这些e
必须可赋值给A
的元素类型。如果A
是多维数组,则这些e
必须自身也是数组取值表达式。如果A
是固定数组类型,并且n
至少为1
,则e_n
可以跟随着, ..
,指出e_n
的值按照需要重复任意多次来填满这个数组。
R{Bindings}
,这里的R
是一个记录类型,而Bindings
是完全同于过程调用的关键字或位置绑定的一个列表。这个绑定列表会被重写来适合R
的字段和缺省值的列表,完全同于过程调用中的那些做为,记录字段名字扮演了过程形式参数的角色。这个构造子指示类型为R
的一个值,它的字段值由重写后的这些绑定来指定。
常量表达式是一般性的表达式类的子集,所要求的限制使得可以静态的求值这个表达式。在常量表达式中,除了NEW()
、解引用(显式的或隐式的)、SUBARRAY()
、TYPECODE()
、NARROW()
、ISTYPE()
、ADR()
和LOOPHOLE()
之外的所有运算都是合法的,唯独可运用的过程,是在提供无符号字运算的Word
接口中的函数。变量在常量表达式中,只能作为给FIRST()
、LAST()
、NUMBER()
、BITSIZE()
、BYTESIZE()
或ADRSIZE()
的实际参数出现,并且这个变量必须不是开放数组类型的。文字和顶层过程常量在常量表达式中是合法的。
指定式
产生变量的表达式叫做指定式(designator)。常量表达式永远不是指定式。指定式的类型是它产生的变量的类型。一个指定式依赖于上下文,可以指示要么一个变量,要么这个变量的值。比如,它作为赋值的左值时是变量,作为右值时是这个变量的值。一些指定式是只读的,它们不能用在可能改变这个变量的值的上下文中,并非只读的指定式就是可写的指定式。
一个标识符是可写的指定式的条件为,它被声明为变量,是VAR
或VALUE
形式参数,TYPECASE
或TRY-EXCEPT
语句的局部变量,或绑定到可写指定式的WITH
局部变量。一个标识符是只读的指定式的条件为,它是READONLY
形式参数,FOR
语句的局部变量,或绑定到非指定式或只读的指定式的WITH
局部变量。只有如下运算可以产生指定式:
r^
,这个运算叫做解引用(dereferencing),它表示r
的所引用者。表达式r^
总是可写的指定式。如果r
的类型是REFANY
、ADDRESS
、NULL
、对象类型或不透明类型,则是一个静态错误;如果r
是NIL
,则是一个必查的运行时间错误。
a[i]
,这个运算叫做下标,它表示数组a
的值的第i + 1 - FIRST(a)
个元素,比如说a
的索引类型是[-1..1]
,a[1]
是这个数组的值的第1 + 1 - (-1)
个即第3
个元素。表达式a[i]
是否为指定式进而是否可写同于a
。表达式i
必须可赋值给a
的索引类型。如果a
是到数组的引用,则这里有隐含的解引用,即这里的a[i]
是a^[i]
的简写。
r.f
、o.f
、I.x
、o.m
、T.m
和E.id
,这种点表示法运算叫选取(selection)。
r.f
表示记录r
的指名字段f
,表达式r.f
是否为指定式进而是否可写同于r
。如果r
是到记录的引用,则这里有隐含的解引用,即r.f
是r^.f
的简写。
o.f
表示对象o
的指名字段f
,表达式o.f
是可写的指定式。
I.x
表示导入的接口I
的指名实体x
,如果x
被声明为变量,则表达式I.x
是指定式并且总是可写的。
o.m
表示对象o
的指名方法m
,o.m
不产生指定式。
T.m
表示对象类型T
的指名方法m
,T.m
不产生指定式。
E.id
表示枚举类型E
的值id
,E.id
不产生指定式。
SUBARRAY(a, from, for)
,它产生数组a
的越过前from
个元素并包含下for
个元素的子数组。SUBARRAY()
不复制数组,它是否为指定式进而是否可写同于a
。如果a
是多维数组,SUBARRAY()
只应用于顶层数组。如果from + for
超过了NUMBER(a)
,则是一个必查的运行时间错误。
子类型规则
使用T <: U
指示T
是U
的子类型,也就是U
是T
的超类型。子类型规则是:如果T <: U
,则类型T
的所有值,都是类型U
的值,反过来则不成立。
对于序数类型T
和U
,T <: U
的条件为,它们拥有相同的基础类型,并且所有的T
的成员都是U
的成员。就是说序数类型上的子类型,反映了在值集合上的子集关系。
对于数组类型,一个数组类型A
是一个数组类型B
的子类型的条件为,它们有相同的最终元素类型,相同的维度数,并且对于每个维度,要么二者都是开放的,要么A
是固定的而B
是开放的,要么它们都是固定的并有着相同的大小。
对于过程类型,T <: U
的条件为,它们除了形式参数名字、缺省值和RAISES
集合之外是相同的,并且T
的RAISES
集合包含在U
的RAISES
集合中。NIL
是所有的过程类型的一个成员。
对于包装类型,BITS n FOR T
和T
有相同的值,也就是说:BITS n FOR T <: T
并且T <: BITS n FOR T
。
对于集合类型[19],SET OF A <: SET OF B
的条件是A <: B
,就是说集合的子类型规则简单的使用值集合规则。
对于引用类型有:
NULL <: REF T <: REFANY
NULL <: UNTRACED REF T <: ADDRESS
就是说,REFANY
和ADDRESS
分别包含所有的有跟踪的和无跟踪的引用,NIL
是所有的引用类型的一个成员。这两个规则也适用于加铭牌的类型。
对于对象类型有:
ROOT <: REFANY
UNTRACED ROOT <: ADDRESS
NULL <: T OBJECT ... END <: T
就是说,所有对象都是引用,NIL
是所有的对象类型的一个成员,而所有的子类型都包括在它的超类型之中。第三个规则也适用于加铭牌的类型。
对于所有的T
,都有T <: T
。对于所有的T, U, V
,T <: U
并且U <: V
,蕴涵T <: V
。就是说<:
是自反和传递的。但是T <: U
并且U <: T
,不蕴涵T
与U
是相同的,因为子类型关系不受形式参数名字、缺省值和包装的影响。
可赋值性
一个类型T
可赋值(assignable)给类型U
的条件是:
T <: U
,或者
U <: T
并且T
是一个数组或除了ADDRESS
之外的一个引用类型(这个限制在不安全模块中解除),或者
T
和U
是至少有一个共同成员的序数类型。
在第一种情况下,没有运行时间错误是有可能的。在第二种情况下,允许赋值一个REFANY
到一个REF T
,还允许赋值一个ARRAY OF T
到一个ARRAY I OF T
,这里分别要求对引用的类型和数组长度的运行时间检查;可以使用类型运算NARROW(x, T): T
,将对象类型或有跟踪的引用类型的超类型变量当作其子类型的成员,这时需要运行时间检查,Modula-2+介入它就是为了以安全的方式,将REFANY
变量赋值给确知的实际类型REF T
。在第三种情况下,常规的范围检查就能确保特定的T
的成员也是U
的成员。
一个表达式e
可赋值给一个变量v
的条件是:
e
的类型可赋值给v
的类型,并且
e
的值是v
的类型的一个成员,它不是一个局部过程,并且如果它是一个数组,则有同v
一样的形状。
一个表达式e
可赋值给类型T
,如果e
可赋值给类型T
的某个变量。如果T
不是一个开放数组,这等同声称e
可赋值给类型T
的任何变量。
语句
执行一个语句产生一个计算,它可以正常产出结果(在可计算理论中称为停机),发起一个例外,导致一个必查的运行时间错误,或无限循环。如果结果是一个例外,它可以可选的配对上一个实际参数。如果一个表达式作为一个语句的执行部份而求值,这个求值引发一个例外,则这个例外成为这个语句的结果。空语句是无操作的(no-op),在语言报告中写为(*skip*)
。
过程调用在这个过程是真正过程的时候是一个语句。调用一个函数过程并丢弃它的结果使用EVAL
语句,它有如下形式:EVAL e
,这里的e
是一个表达式。它的效果是求值e
并忽略其结果。RETURN
语句用于从过程中返回。
赋值语句有如下形式:v := e
,这里的v
是可写的指定式,而e
是可赋值给v
所指示变量的表达式。赋值语句将v
设置为e
的值,v
和e
的求值次序是未定义的,但e
会在v
更新之前求值。特别是,如果v
和e
是有交叠的子数组,以没有元素在被用作来源之前被用作目标的方式进行赋值。
顺序复合语句有如下形式:S_1; S_2
,它在Modula-2中称为“语句序列”。一些编程者使用分号作为语句终结符(terminator),另一些编程者将它用作分隔符(separator);Modula-3允许这两种风格,语言报告将其用作分隔符。
块语句有如下形式:Decls BEGIN S END
,这里的Decls
是一序列的声明,S是一个语句。块介入在Decls
中声明的常量、类型、变量和过程,并接着执行S
。声明的名字的作用域是这个块。
选择控制结构
Modula-3提供了选择控制结构IF
和CASE
语句。IF
语句有如下形式:
IF B_1 THEN S_1
ELSIF B_2 THEN S_2
...
ELSIF B_n THEN S_n
ELSE S_0
END
这里的这些B
是布尔表达式,而这些S
是语句。ELSE S_0
和每个ELSIF B_i THEN S_i
都是可选的。IF
语句依次求值B
,直到某个B_i
求值为TRUE
,则接着执行S_i
。如果没有表达式求值为TRUE
,并且ELSE S_0
存在,则执行S_0
。如果没有表达式求值为TRUE
,并且ELSE S_0
缺席,则IF
语句是无操作的(no-op)。Modula-3的CASE
语句,不同于Modula-2的同名语句,它有如下形式:
CASE Expr OF
L_1 => S_1
| ...
| L_n => S_n
ELSE S_0
END
这里的Expr
是类型为序数类型的一个表达式,而每个L
是一个列表,这个列表构成自常量表达式,或用e_1..e_2
指示的常量表达式的范围,它表示从e_1
到e_2
含二者的值。如果e_1
超出了e_2
,这个范围为空。如果两个L
表示的集合有交叠,或者任何常量表达式不是类型Expr
的一个成员,则是一个静态错误。ELSE S_0
是可选的。
CASE
语句求值Expr
。如果结果的值在任何L_i
之中,则执行S_i
。如果这个值不在任何L_i
之中,并且ELSE S_0
存在,则执行它。如果这个值不在任何L_i
中,并且ELSE S_0
缺席,则发生一个必查的运行时间错误。一些编程者使用竖杠作为初始符(initiator),另一些编程者将它用作分隔符。Modula-3允许这两种风格,语言报告将其用作分隔符。
分配类型测试
TYPECASE
语句有如下形式:
TYPECASE Expr OF
T_1 (v_1) => S_1
| ...
| T_n (v_n) => S_n
ELSE S_0
END
这里的Expr
是类型为引用类型的一个表达式,这些S
是语句,这些T
是引用类型,而这些v
是标识符。如果Expr
有类型ADDRESS
,或者任何T
不是Expr
的类型的子类型,则为一个静态错误。ELSE S_0
和每个(v)
都是可选的。如果(v_i)
缺席,形式T_1, ..., T_n => S
,是T_1 => S | ... | T_n => S
的简写。
TYPECASE
语句求值Expr
,如果结果的引用值,是任何列出的类型T_i
的成员,则针对其中最小的i
,执行S_i
。NIL
是所有引用类型的一个成员,因此NULL
情况只有首先出现才能起作用。如果这个值不是列出的这些类型的一个成员,并且ELSE S_0
存在,则执行它。如果这个值不是列出的这些类型的一个成员,并且ELSE S_0
缺席,则发生一个必查的运行时间错误。每个(v_i)
声明类型为T_i
的一个变量,并且它的作用域是S_i
。如果v_i
存在,它在执行S_i
之前被初始化为Expr
的值。
可以使用TYPECASE
语句测试类型为REFANY
或对象的一个表达式的值的引用类型,比如写一个过程ToText(r: REFANY): TEXT
,在其中测试r
的值是NULL
、REF BOOLEAN
和REF INTEGER
中的哪一个类型。
循环控制结构
Modula-3提供了循环控制结构FOR
、LOOP
、WHILE
和REPEAT
语句,和必须在字面上包围在这四个循环语句中,用于从循环中退出的EXIT
语句。语句LOOP S END
,循环直到S
中有一个EXIT
发生。语句WHILE B DO S END
,被定义为:
LOOP
IF B THEN S
ELSE EXIT
END
END
语句REPEAT S UNTIL B
,被定义为:
LOOP
S;
IF B THEN EXIT END
END
FOR
语句有如下形式:FOR id := first TO last BY step DO S END
,这里的id
是一个标识符,而first
和last
是有相同基础类型的序数表达式,step
是整数取值的表达式。而S
是一个语句。BY step
是可选的,如果省略,step
缺省为1
。标识符id
指示其作用域是S
的一个只读变量,其类型是first
和last
共同的基础类型。FOR
语句的语义,可如下这样用WHILE
和WITH
语句精确的定义为一个块:
VAR
i := ORD(first);
done := ORD(last);
delta := step;
BEGIN
IF delta >= 0 THEN
WHILE i <= done DO
WITH id = VAL(i, T) DO S END;
INC(i, delta)
END
ELSE
WHILE i >= done DO
WITH id = VAL(i, T) DO S END;
INC(i, delta)
END
END
END
这里的T
是id
的类型,i
、done
和delta
代表不出现在FOR
语句中的变量。从FOR
语句的这个定义可以看出,如果一个FOR
循环的上界是LAST(INTEGER)
或LAST(LONGINT)
,就应当将它改写为WHILE
循环来避免溢出。
With语句
Modula-3的WITH
语句,无关于Modula-2的同名语句,它有如下形式:WITH id = e DO S END
,这里的id
是一个标识符,e
是一个表达式,而S
是一个语句。WITH
语句声明了具有作用域S
的id
,作为变量e
的别名,或作为值e
的只读名字。表达式e
在进入WITH
语句的时候被求值一次。一个单一的WITH
语句可以包含多个绑定,它们顺序的求值,就是说WITH id_1 = e_1, id_2 = e_2, ...
,等价于WITH id_1 = e_1 DO WITH id_2 = e_2 DO ....
。
WITH
语句可类比于某种过程调用P(e)
,这里的P
被声明为:
PROCEDURE P(mode id: typeof e) =
BEGIN
S
END P
这里的算符typeof
只是个示意,它不在语言规定之中;如果e
是可写的指定式,则mode
是VAR
,否则mode
是READONLY
。在WITH
语句和调用P(e)
之间唯一的区别为,出现在WITH
语句内的自由变量、RETURN
和EXIT
,是在WITH
语句所在的上下文中解释的,而非在新增加的P
的上下文中。
Modula-3语言报告使用WITH
语句定义其他语句,比如,INC
和DEC
语句分别有如下形式:INC(v, n)
和DEC(v, n)
,这里的v
指示一个序数类型的变量,而n
是可选的一个整数取值表达式,如果省略n
则缺省为1
,这两个语句分别等价于WITH x = v DO x := VAL(ORD(x) + n, T) END
和WITH x = v DO x := VAL(ORD(x) - n, T) END
,这里的T
是v
的类型,而x
代表不出现在n
中的变量。
例外
例外声明有如下形式:EXCEPTION id(T)
,这里的id
是一个标识符,而T
是除了开放数组之外的一个类型,它声明了id
是具有实际参数类型T
的例外。如果省略了(T)
,这个例外不接受实际参数。例外声明只允许出现在接口和模块的最外层作用域之中。所有的声明的例外都是各不相同的。
RAISE
语句有两种形式,没有实际参数的形式:RAISE e
,这里的e
是不接受实际参数的一个例外,它的结果是发起例外e
;具有实际参数的形式:RAISE e(x)
,这里的e
是接受实际参数的一个例外,而x
是可赋值给e
的实际参数类型的一个表达式,它的结果是引发具有配对实际参数x
的例外e
。
Modula-3的例外处理,基于了TRY-EXCEPT
语句。采用类似的块系统的有Delphi、Python[20]、Scala[21]和Visual Basic.NET。 TRY-EXCEPT
语句有如下形式:
TRY
Body
EXCEPT
id_1 (v_1) => Handler_1
| ...
| id_n (v_n) => Handler_n
ELSE Handler_0
END
这里的Body
和每个Handler
都是语句,每个id
指名一个例外,而每个v_i
是一个标识符。ELSE Handler_0
和每个(v_i)
都是可选的。每个(v_i)
声明一个变量,其类型是例外id_i
的实际参数类型,而它的作用域是Handler_i
。如果(v_i)
缺席,形式id_1, ..., id_n => Handler
,是id_1 => Handler; ...; id_n => Handler
的简写。
TRY
子句执行Body
。如果结果是正常的,则忽略EXCEPT
等子句。如果Body
引发任何列出的例外id_i
,则执行Handler_i
。在处理具有配对实际参数x
的例外id_i
的时候,v_i
在执行Handler_i
之前被初始化为x
。如果Body
引发任何其他例外,并且ELSE Handler_0
存在,则执行它。在这两种情况的任何之一下,TRY-EXCEPT
语句的结果是选出的处理器的结果。如果Body
引发未列出的例外,而ELSE Handler_0
缺席,则TRY-EXCEPT
语句的结果是Body
引发的例外。
Modula-3还提供TRY-FINALLY
语句,它有如下形式:TRY S_1 FINALLY S_2 END
,它执行语句S_1
接着执行语句S_2
。如果S_1
的结果是正常的,则TRY
语句的结果等价于S_1; S_2
。如果S_1
的结果是例外,并且S_2
的结果是正常,则在S_2
执行之后,重新发起来自S_1
的例外。如果二者的结果都是例外,则TRY
语句的结果是来自S_2
的例外。
在Modula-3中,EXIT
和RETURN
语句的语义,分别被定义为发起叫做exit-exception
和return-exception
的例外。exit-exception
不接受实际参数,return-exception
接受任意类型的实际参数,程序不能显式的命名这些例外。语句LOOP S END
,非正式的等价于:
TRY
S; S; S; ...
EXCEPT
exit-exception => (*skip*)
END
过程
过程类型
一个过程要么是NIL
,要么是有如下构成的三元组:
- 过程体,它是一个语句。
- 签名,它有如下形式
(formal_1; ...; formal_n): R RAISES S
,这里的每个formal_i
是形式参数,R
是结果类型,S
是RAISES
集合(这个过程可以引发的例外的集合)。
- 环境,它是在解释过程体中变量名字方面有关的作用域。
形式参数有如下形式:Mode Name: Type := Default
,这里的:
Mode
是参数模态,它可以是VALUE
、VAR
或READONLY
。如果省略了Mode
,它缺省为VALUE
。
Name
是命名这个形式参数的标识符。形式参数名字必须是各不相同的。
Type
是这个形式参数的类型。
Default
是一个常量表达式,它是这个形式参数的缺省值。如果Mode
是VAR
,则:= Default
必须省略,否则:= Default
和: Type
中任何一个都可以省略,但不能二者都省略。如果省略了Type
,它采用Default
的类型。如果二者都存在,Default
的值必须是Type
的一个成员。
形式Mode v_1, ..., v_n: Type := Default
,是Mode v_1: Type := Default; ...; Mode v_n: Type := Default
的简写。
VAR
形式参数绑定到对应的实际参数所指示的变量上,也就是说它是别名。对于VAR
形式参数,实际参数必须是可写的指定式,它的类型同于形式参数的类型,或者在VAR
数组形式参数的情况下,它可赋值给这个形式参数。
VALUE
形式参数绑定到具有未用的位置的一个变量上,并且它被初始化为实际参数对应的值。对于VALUE
或READONLY
形式参数,实际参数是可赋值给形式参数类型的任何表达式,并且放开了针对赋值局部过程的禁止。
READONLY
形式参数,如果实际参数是指定式并且有与形式参数相同的类型,或者是可赋值给形式参数的类型的一个数组类型,则被当作VAR
形式参数对待,它的只读语义体现在指定这个指定式为只读的,否则被当作VALUE
形式参数对待。
在签名中,如果省略了: R
,则这个过程是真正(proper)过程,否则为函数过程;如果省略了RAISES S
,则假定它为RAISES {}
。一个过程发起未包括在RAISES
集合中的一个例外,是一个必查的运行时间错误,如果语言实现将这个运行时间错误映射成一个例外,则这个例外隐含的包括在所有的RAISES
子句中。
过程声明
有两种形式的过程声明:只允许出现在接口中的PROCEDURE id sig
,和只允许出现在模块中的PROCEDURE id sig = B id
,这里的id
是一个标识符,sig
是过程签名,而B
是一个块。在一个模块的最外层作用域内声明的过程是顶层过程,其他过程是局部过程。局部过程可以作为形式参数传递但不能被赋值,因为在栈实现下,局部过程在包含它的栈桢被弹出后会成为无效的。
过程类型声明有如下形式:TYPE T = PROCEDURE sig
,这里的sig
是签名规定。一个过程P
是过程类型T
的一个成员,或称为是它的一个值的条件为,它是NIL
,或者它的签名被T
的签名所含盖(cover)。这里的签名sig_A
含盖签名sig_B
的条件是:
- 它们有相同数目的形式参数,并且对应的形式参数有相同的类型和模态。
- 它们有相同的结果类型,或者都没有结果类型。
sig_A
的RAISES
集合包含sig_B
的RAISES
集合。
过程常量是声明为过程的一个标识符。过程变量是声明为具有过程类型的一个变量。例如:
PROCEDURE P(txt: TEXT := "P") =
BEGIN
IO.Put(txt)
END P;
VAR q: PROCEDURE(txt: TEXT := "Q") := P;
声明了过程常量P
和过程变量q
。两个过程如果有一致的闭包,就是说它们涉及相同的过程体和环境,则二者相等即有相同的值。形式参数名字和缺省值,影响一个过程的类型即签名,而非这个过程的值。将一个过程常量赋值给一个过程变量,改变这个它的值,而非它的类型。比如上例中有:P = q
为TRUE
。缺省的形式参数的解释,由一个过程的类型来确定,而非这个过程的值。比如上例子中:P()
打印P
,而q()
打印Q
。
过程调用
过程调用有如下形式:P(Bindings)
,这里的P
是一个过程取值的表达式,而Bindings
是关键字或位置绑定的一个列表。关键字绑定有如下形式:name := actual
,这里的actual
是一个表达式,而name
是一个标识符。位置绑定有如下形式:actual
,这里的actual
是一个表达式。当关键字和位置绑定混合在一个调用中的时候,位置绑定必须前导于关键字绑定。如果绑定列表为空,圆括号仍然是需要的。
绑定列表要如下这样重写,来适合P
的类型签名:首先,每个位置绑定actual
,若为Bindings
中的第i
个绑定,则通过补充上第i
个形式参数的名字,转换成关键字绑定增加到关键字绑定列表中。其次,对于有缺省值并且在第一步之后没有被绑定的每个形式参数,将它的形式参数的名字name
和它的缺省值default
,形成关键字绑定name := default
增加到关键字绑定列表。重写成的关键字绑定列表必须只绑定形式参数,并且必须只绑定每个形式参数正好一次。
执行过程调用,需要求值过程取值表达式P
和它的实际参数,绑定形式参数,并执行过程体。P
和它的实际参数的求值次序是未定义的。调用一个未定义或NIL
过程,是一个必查的运行时间错误。调用具有过程体B
的真正过程,在绑定了实际参数之后等价于:
TRY
B
EXCEPT
return-exception => (*skip*)
END
而调用具有过程体B
的函数过程等价于:
TRY
B; (错误:没有返回值)
EXCEPT
return-exception (v) => (结果成为v)
END
递归定义
对于常量、类型或过程声明N = E
、变量声明N: E
、例外声明N(E)
或揭示N = E
,如果N
出现在E
的任何部份展开之中,则它是递归的。对于省略了类型的变量声明N := I
,如果N
出现在I
的类型E
的任何部份展开中,则它是递归的。允许这种声明的条件,是N
在E
的任何部份展开中所有的出现为如下三者之一:在类型构造子REF
或PROCEDURE
的某个出现之内,在类型构造子OBJECT
的一个字段或方法类型之内,或者在一个过程体之内。下面是合法的递归声明的例子:
TYPE
RealList = REF RECORD
val: REAL;
link: RealList
END;
ListNode = OBJECT
link: ListNode
END;
IntList = ListNode OBJECT
val: INTEGER
END;
T = PROCEDURE(n: INTEGER; p: T);
EXCEPTION E(PROCEDURE() RAISES {E});
PROCEDURE P(b: BOOLEAN) =
BEGIN
IF b THEN P(NOT b) END
END P;
特征性技术
Modula-3的设计者认为Modula-2最成功的特征,是提供了在模块之间的显式接口,故而接口在Modula-3中保留而在本质上没有变化。到模块的接口,是披露了模块公开部份的声明搜集,而未声明于接口中的模块中的东西是私有的。模块导入它所依赖的接口,而导出它所实现的接口(在Modula-3中可以是多个接口)。
在现代编程语言的实现下,一个抽象类型的值,被表示一个对象,它的操作由叫做对象的方法的过程值的一个套件来实现。一个新的对象类型,可以定义为现存类型的子类型,在这种情况下新类型拥有旧类型的所有方法,并且可能还有新的方法(继承)。新类型可以为旧方法提供新的实现(覆盖)。对象类型和Modula-2的不透明类型的组合,产生了一个新事物:部份不透明(partially opaque)类型,这里的一个对象的一些字段在一个作用域内是可见的,而其他的是隐藏的。
泛型模块是一种模板,在其中一些导入的接口被当作形式参数,它们在实例化泛型的时候要绑定到实际接口。不同的泛型实例是独立编译的,源代码程序被重复使用,而编译后的代码对不同实例通常是不同的。为保持简单性,在Modula-3中泛型被限制在模块层面,泛型的过程和类型不孤立存在,并且泛型形式参数必须都是完全的接口。
将计算分解入并发进程(或线程)中是关注点分离的基本方法。Modula-2提供的线程很弱,本质上相当于协程。Modula-3采用了霍尔的监视器,它是并发编程的更坚实基础。Modula-2+在设计中加入了简化的Mesa同步原语,它足够成功而在实质上没有改变的结合入了Modula-3。
一个语言特征是不安全的,如果它的滥用可以破坏运行时系统,使得程序的进一步运行不忠实于语言的语义,不安全特征的一个例子,是没有边界检查的数组赋值。一个安全的程序的一个错误,可以导致计算中止,并给出运行时间错误信息或给出错误解答,但它不会导致计算分崩离析。Modula-3在这个领域里跟从Cedar,接受只在显式的标记为不安全的模块中才允许的少量不安全特征。
模块化
接口中的声明,除了任何变量初始化必须是常量,和过程声明必须只能指定签名而不能有过程体之外,同于在块中的声明。接口有如下形式:
INTERFACE id;
Imports;
Decls
END id.
这里的id
是命名这个接口的标识符,Imports
是一序列的导入语句,而Decls
是一序列的声明。在Decls
中的名字和可见的导入的名字必须是各不相同的。两个或更多个接口形成导入环是静态错误。典型的接口定义,通常为每个接口定义一个数据结构(记录或对象)以及任何的支持过程。
模块除了实体名字的可见性之外,就像是一个块。一个实体在一个块中是可见的,如果它被声明在这个块中,或在某个包围的块中;而一个实体在一个模块中是可见的,如果它被声明在这个模块中,或在这个模块所导入或导出的一个接口中。模块有如下形式:
MODULE id EXPORTS Interfaces;
Imports;
Block id.
这里的id
是命名这个模块的一个标识符,Interfaces
是这个模块导出的那些接口的不同名字的一个列表,Imports
是一序列的导入语句,而Block
是一个块,它是模块体。名字id
必须重复于终结模块体的END
之后。EXPORTS Interfaces
可以省略,在这种情况下Interfaces
缺省为id
,即模块将缺省导出相同名称的接口。Modula-3中的导出EXPORTS
,与Modula-2中的导出EXPORT
无关,它在概念上对应于Modula-2的“实现”。
如果模块M
导出了接口I
,则在I
中声明的所有名字在M
中是不加以限定而可见的。一个模块M
导出一个接口I
,可以重新声明在这个接口中声明的一个或多个过程,为其提供过程体。在M
中的签名,必须被在I
中的签名所含盖。要确定在对这个过程的调用中的关键字绑定的解释,在M
内使用M
中的签名,在其他地方使用I
中的签名。除了重新声明导出的过程之外,在Block
的顶层中声明的名字,可见的导入的名字,和在导出的接口中声明的名字必须是各不相同的。
一个程序的模块和接口的名字叫做全局名字。查找全局名字的方法是依赖于实现的,比如通过文件系统查找路径。一个模块或接口X
导入一个接口I
,从而使得在I
中声明的实体和揭示在X
中可见,但不包括I
所导入的那些。一个模块M
使用了接口I
,如果M
导入或导出了I
,或者M
使用了导入I
的一个接口。Modula-3中所有的编译单元,要么是接口要么是模块。任何编译单元都可以使用IMPORT
语句从其他接口导入标识符。与其他语言的包含功能不同,任何标识符都可以追踪到它的起源位置。
IMPORT
语句有如下的典型使用形式:IMPORT I
,它导入全局名字为I
的接口,并给它一个同样的局部名字,从而使用点表示法访问接口中的实体(类似于访问记录中的字段)。比如接口中的主要类型按惯例命名为T
,则使用它时采用的限定形式为I.T
。如果导入接口的名字与模块内的其他实体的名字发生冲突,则可以使用IMPORT I AS X
这样的形式给与它不同的局部名字。
IMPORT
语句还有如下的另一种形式:FROM I IMPORT N
,它为在接口I
中声明为N
的实体,介入一个局部名字N
,比如:FROM IO IMPORT Put
,并且不加限定的使用它,比如:Put("Hello World\n")
。局部绑定优先于全局绑定,例如:IMPORT I AS J, J AS I; FROM I IMPORT N
,为全局名字为J.N
的实体介入了局部名字N
。非法使用相同的局部名字两次是静态错误。
一个程序是模块和接口的搜集,包含了任何它的模块或接口所导入或导出的所有接口,并且在其中没有重复定义的过程、模块或接口。执行一个程序的效果是执行它的所有模块的模块体。模块的执行次序受到初始化规则的约束:如果模块M
依赖于模块N
并且N
不依赖于M
,则N
的模块体将先于M
的模块体执行。这里的模块M
依赖于模块N
,如果M使用了N
导出的一个接口,或者M
所依赖的一个模块依赖于N
。
其模块体最后执行的模块叫做主模块。在初始化规则不能唯一的确定它的情况下,语言实现提供了指定这个主模块的方法,即主模块是导出Main
的模块,它的内容是依赖于实现的。程序执行在主模块终止的时候终止,即使并发的控制线程仍在执行。
下面是接口与模块的一个规范性的例子,是有隐藏表示的一个公开堆栈:
INTERFACE Stack;
TYPE T <: REFANY;
PROCEDURE Create(): T;
PROCEDURE Push(VAR s: T; x: REAL);
PROCEDURE Pop(VAR s: T): REAL;
END Stack.
MODULE Stack;
REVEAL T = BRANDED OBJECT
item: REAL;
link: T
END;
PROCEDURE Create(): T =
BEGIN
RETURN NIL
END Create;
PROCEDURE Push(VAR s: T; x: REAL) =
BEGIN
s := NEW(T, item := x, link := s)
END Push;
PROCEDURE Pop(VAR s: T): REAL =
VAR res: REAL;
BEGIN
res := s.item;
s := s.link;
RETURN res
END Pop;
BEGIN
END Stack.
如果多于一个模块需要此堆栈的表示,则应当把它转移到私有接口中,从而在需要它的任何地方的都可以导入它:
INTERFACE Stack;
(* ... 同上 ... *)
END Stack.
INTERFACE StackRep;
IMPORT Stack;
REVEAL Stack.T = BRANDED OBJECT
item: REAL;
link: Stack.T
END
END StackRep.
MODULE Stack;
IMPORT StackRep;
(* Push、Pop和Create同上 *)
BEGIN
END Stack.
对象类型
一个对象要么是NIL
,要么是到有配对的方法套件的一个数据记录的引用,方法套件是接受这个对象作为第一个实际参数的那些过程的一个记录。这个数据记录可以包含经由对象类型的子类型介入的增加字段,这个套件可以包含经由子类型介入的增加方法。对象赋值是引用赋值。要复制一个对象的数据记录到另一个对象,这些字段必须单独的赋值。
Modula-3意图保持采用对象这个最简单的术语,而非在其他面向对象语言中对应的术语类。对象类型声明采用如下形式:
TYPE T = ST OBJECT
Fields
METHODS
Methods
OVERRIDES
Overrides
END
这里的ST
是可选的超类型,如果省略则缺省为ROOT
,Fields
是完全如同记录类型那样的字段声明的一个列表,Methods
是方法声明的一个列表,而Overrides
是方法覆盖的一个列表。在字段和方法声明中介入的名字,必须相互不同,并且不同于在覆盖声明中的名字。T
的字段构成自ST
的字段和随后的新声明的字段。T
的方法构成自经过OVERRIDES
中的覆盖修改的ST
的方法,和随后的METHODS
中声明的方法。T
有着同ST
一样的引用类。
关键字OBJECT
可选的可以前导上BRANDED
或BRANDED b
来给对象加铭牌,使之唯一而避免结构等价,这里的b
是文本常量。其含义同于非对象的引用类型,b
被省略时,系统自动生成唯一性的一个字符串。
方法声明有如下形式:m sig := proc
,这里的m
是个标识符,sig
是过程签名,而proc
是顶层过程常量。方法声明指定了T
的方法m
有签名sig
和值proc
。如果省略了:= proc
假定它为:= NIL
。如果proc
非空,它的第一个形式参数必须是缺省的模态VALUE
,并有着T
的某个超类型的类型(包括且经常是T
自身),并且去除了第一个形式参数结果就是sig
所含盖的签名。
方法覆盖(override)有如下形式:m := proc
,这里的m
是超类型ST
的方法的名字,而proc
是顶层过程常量。方法覆盖指定了T
的方法m
是proc
,并非指定ST.m
。如果proc
非空,它的第一个形式参数必须是缺省的模态VALUE
,并有着T
的某个超类型的类型(包括且经常是T
自身),去除了proc
第一个形式参数结果就是ST
的m
方法所含盖的签名。举例说明:
TYPE
Super = OBJECT
a: INTEGER;
METHODS
p()
END;
Sub = Super OBJECT
b: INTEGER
END;
PROCEDURE ProcSuper(self: Super) = ... ;
PROCEDURE ProcSub(self: Sub) = ... ;
这里的过程ProcSuper
和ProcSub
是类型Super
和Sub
的对象的p
方法的候选值。例如:
TYPE T1 = Sub OBJECT
OVERRIDES
p := ProcSub
END
|
TYPE T2 = Super OBJECT
OVERRIDES
p := ProcSuper
END
|
声明了具有Sub 数据记录和预期一个Sub 的p 方法的一个类型。T1 是Sub 的有效的子类型。
|
声明了具有Super 数据记录和预期一个Super 的p 方法的一个类型。T2 是Super 的有效的子类型。
|
TYPE T3 = Sub OBJECT
OVERRIDES
p := ProcSuper
END
|
TYPE T4 = Super OBJECT
OVERRIDES
p := ProcSub
END
|
声明了具有Sub 数据记录和预期一个Super 的p 方法的一个类型。因为所有Sub 都是Super ,这个方法不挑剔要放入的对象。
|
尝试声明具有Super 数据记录和预期一个Sub 的p 方法的一个类型,因为不是所有Super 都是Sub ,这个方法很挑剔要放入的对象。T4 的声明是个静态错误。
|
如果T
是对象类型或到记录的引用,NEW(T)
操作有如下形式:NEW(T, Bindings)
,这里的Bindings
是用来初始化新字段的关键字绑定的一个列表,不允许位置绑定。每个绑定f := v
将字段f
初始化为值v
。如果T
是对象类型,则Bindings
还可以包括形如m := P
的方法覆盖,这里的m
是T
的一个方法而P
是一个顶层过程常量。NEW(T, m := P)
是NEW(T OBJECT OVERRIDES m := P END)
的语法糖。
如果o
是一个对象,则o.f
指示在o
的数据记录中名为f
的数据字段。o.f
是可写的指定式,它的类型是这个字段的所声明的类型。如果m
是o
方法之一,以o.m(Bindings)
形式的过程调用,指示执行o
的m
方法,这等价于:(o的m方法) (o, Bindings)
。
如果T
是对象类型而m
是T
的方法的名字之一,则T.m
指示T
的m
方法。它的过程类型的第一个实际参数有类型T
,而第一个实际参数对应的形式参数名字是未指定的,因此在调用T.m
时,第一个实际参数必须按位置来指定,不能用关键字。这种表示法便于子类型方法调用它的某个超类型的对应方法。
在子类型中的字段或方法遮盖(mask)了在超类型中的任何同名的字段或方法。要访问被遮盖的字段,使用类型运算NARROW(x, T)
来将子类型变量当作超类型的成员,这里的T
必须是对象类型或有跟踪的引用类型,而x
必须可赋值给T
。NARROW(x, T): T
在检查了x
是T
一个成员之后返回x
,如果检查失败则发生运行时间错误。
下面的例子展示了在声明新方法和覆盖现存方法之间的不同,首先声明如下:
TYPE
Super = OBJECT
METHODS
m() := ProcSuper
END;
SubOverridden = Super OBJECT
OVERRIDES
m := ProcSub
END;
SubExtended = Super OBJECT
METHODS
m() := ProcSub
END;
VAR
a := NEW(Super);
b := NEW(SubOverridden);
c := NEW(SubExtended);
然后可以用a.m()
激活ProcSuper(a)
,用b.m()
激活ProcSub(b)
,用c.m()
激活ProcSub(c)
,如此调用在覆盖和扩展之间没有区别。但是扩展的c
的方法套件有两个方法,而覆盖的b
的方法套件只有一个方法,这可以通过使用NARROW(x, T)
将子类型的变量b
和c
视为超类型Super
的成员来披露:NARROW(b, Super).m()
激活ProcSub(b)
,NARROW(c, Super).m()
激活ProcSuper(c)
。
对象不能解引用,因为在语言实现中,一个对象变量的静态类型不确定它的数据记录的类型。对象类型确定了关乎数据记录字段和方法套件的超类型链的前缀的那些类型。
实现
下面是语言设计者提出的对象的一种可能实现的梗概[19],一个对象可以表示为它的数据记录的第一个字的地址。前面的字存储一个对象头部,它包含一个唯一于对象类型的类型代码。这些类型代码是小型整数,对每个对象类型和每个有跟踪的引用类型都有一个代码。在对象头部之前的字,存储到这个对象的方法套件的一个引用。如果这个对象没有方法,这个字可以省略。这还允许对象共享方法套件,这是常见情况。如果o
是一个对象,d
是它的数据字段之一,而m
是它的方法之一,则在这种表示下:
o.d |
是 |
Mem[o + d]
|
o.m |
是 |
Mem[Mem[o – 2] + m]
|
TYPECODE(o) |
是 |
Mem[o – 1]
|
这里假定了字段和方法以自然方式表示为偏移量。进一步的问题是如何高效的测试一个对象o
是否有着类型T
,这是NARROW()
和TYPECASE
语句所需要的。NARROW()
的最简单实现,是维护一个以类型代码为索引的数组st
,st[tc]
是其类型代码为tc
的对象类型的超类型的类型代码,如果它没有超类型则为NIL
。要测试o
是否是一个T
,使用一个循环来计算T
的类型代码是否出现在如下序列之中:
TYPECODE(o), st[TYPECODE(o)], st[st[TYPECODE(o)]], ... NIL
这个序列可以称为o
的类型的超类型路径,而它的长度可称为o
的类型的深度。利用上每个类型的深度都是编译时间确定的,因此可以同相应的类型代码一起存储的事实,可以有更快速的NARROW()
实现。如果T
的类型代码,出现在类型U
的超类型路径上,它就该在位置depth(U) - depth(T)
上。如果每个类型的超类型路径都是顺序数组,这意味着NARROW()
可以用恒定时间实现。因为超类型路径通常不太长,这是一个有吸引力的策略。在不常见的对象类型有非常长的超类型链的情况下,只有这个链的直到某个极大长度的一个前缀,会被顺序的存储。在运行时间,这个深度差如果超出这个链的顺序存储的长度,实现必须回退到链表。
不透明类型及其披露
不透明(opaque)类型声明有如下形式:TYPE T <: U
,这里的T
是标识符,而U
是指示一个引用类型的表达式。它将名字T
介入为不透明类型,并披露了U
是T
的超类型。
Modula-3的揭示(revelation)机制,将关于一个不透明类型的信息,介入到一个作用域之内,不同于其他声明,揭示不介入新的名字。不同的作用域,可以披露(reveal)关于一个不透明类型的不同的信息。例如,某个不透明类型,在一个作用域内知晓为REFANY
的子类型,在其他的作用域内可以知晓为ROOT
的子类型。
不透明类型名字所指示的实际类型叫做具体类型。T
的具体类型,必须披露于程序的其他地方。在对象声明中,如果ST
被声明为不透明类型,则只在知晓ST
的具体类型为对象类型的作用域内,T
的声明是合法的。如果T
被声明为不透明类型,则只在完全知晓T
的具体类型,或者知晓它为对象类型的作用域内,NEW(T)
是合法的。
揭示分为两种:部份的和完全的,一个程序可以包含一个不透明类型的任意多个部份揭示,但必须包含唯一的一个完全揭示。
部份揭示有如下形式:REVEAL T <: V
,这里的V
是类型表达式(可能就是个名字),而T
是被声明为不透明类型的一个标识符(可能有限定)。它披露了V
是T
的超类型。在任何作用域内,对一个不透明类型披露的超类型,必须是在子类型关系下是线性有序的。就是说,如果披露了T <: U1
和T <: U2
,则必须也要披露要么U1 <: U2
要么U2 <: U1
。REVEAL T <: T
是合法的非递归声明。
完全揭示有如下形式:REVEAL T = V
,这里的V
是类型表达式(不能就是个名字),它的最外层类型构造子,是有铭牌的一个引用或对象类型,而T
是被声明为不透明类型的一个标识符(可能有限定)。这个揭示指定了V
是T
的具体类型。在任何作用域内,披露为T
的超类型的任何类型,都必须是V
的超类型,否则是一个静态错误。不同的不透明类型有不同的具体类型,因为V
包含了一个铭牌并且在程序中所有的铭牌都是独一的。REVEAL I.T = I.T BRANDED OBJECT ... END
是非法的递归声明。
揭示只允许用在接口和模块的最外层作用域内。在接口中的揭示可以被导入到任何需要它的作用域之内。揭示提供了向客户隐藏实现细节的,在概念上简单清晰却非常强力的机制,例如:
INTERFACE I;
TYPE T <: ROOT;
PROCEDURE P(x: T): T;
END I.
INTERFACE IRep;
IMPORT I;
REVEAL I.T = MUTEX BRANDED OBJECT
count: INTEGER
END;
END IRep.
INTERFACE IClass;
IMPORT I;
REVEAL I.T <: MUTEX;
END IClass.
导入I
的编译单元见到的I.T
是ROOT
的不透明子类型,因而被限制为可以分配类型I.T
的对象,把它们传递给I.P
,或声明I.T
的子类型。导入IRep
的编译单元见到的I.T
是具体类型,它是有扩展的count
字段的MUTEX
的子类型。导入IClass
的编译单元见到的I.T
是MUTEX
的不透明子类型,因而可以锁定类型I.T
的对象。
泛型
在泛型接口或模块中,某些导入的接口名字被当作形式参数,它们在泛型被实例化的时候要绑定到实际接口上。泛型接口及其实例有如下左侧的形式,并等价于右侧定义的普通接口:
GENERIC INTERFACE G(F_1, ..., F_n);
Body
END G.
INTERFACE I = G(A_1, ..., A_n) END I.
|
INTERFACE I;
IMPORT A_1 AS F_1, ..., A_n AS F_n;
Body
END I.
|
这里的G
是命名这个泛型接口的一个标识符,F_1, ..., F_n
是叫做G
的形式导入的标识符的一个列表,而Body
同在非泛型接口中一样,是一序列导入和随后的一序列声明。这里的I
是这个实例的名字,而A_1, ..., A_n
是G
的形式导入要绑定的实际接口的一个列表。
泛型模块及其实例有如下左侧的形式,并等价于右侧定义的普通模块:
GENERIC MODULE G(F_1, ..., F_n);
Body
END G.
MODULE I EXPORTS E = G(A_1, ..., A_n) END I.
|
MODULE I EXPORTS E;
IMPORT A_1 AS F_1, ..., A_n AS F_n;
Body
END I.
|
这里的G
是命名这个泛型模块的一个标识符,F_1, ..., F_n
是叫做G
的形式导入的标识符的一个列表,而Body
同在非泛型模块中一样,是一序列的导入和随后的一个块。这里的I
是这个实例的名字,E
是由M
导出的接口的一个列表,而A_1, ..., A_n
是G
的形式导入要绑定的实际接口的一个列表。EXPORTS E
可以省略,在这种情况下它缺省为EXPORTS I
。泛型模块自身没有导出,它们只在被实例化时提供。
泛型接口及其对应的泛型模块(与C++模板一样)可以轻松定义和使用抽象数据类型,但粒度在模块级别,裸露的类型INTEGER
或REAL
不能使用,相比之下,它们在C++模板中可以使用。例如,可以定义泛型堆栈:
GENERIC INTERFACE Stack(Elem);
(* 这里的Elem.T不是开放数组类型 *)
TYPE T <: REFANY;
PROCEDURE Create(): T;
PROCEDURE Push(VAR s: T; x: Elem.T);
PROCEDURE Pop(VAR s: T): Elem.T;
END Stack.
GENERIC MODULE Stack(Elem);
REVEAL T = BRANDED OBJECT
n: INTEGER;
a: REF ARRAY OF Elem.T
END;
PROCEDURE Create(): T =
BEGIN
RETURN NEW(T, n := 0, a := NIL)
END Create;
PROCEDURE Push(VAR s: T; x: Elem.T) =
BEGIN
IF s.a = NIL THEN
s.a := NEW(REF ARRAY OF Elem.T, 5)
ELSIF s.n > LAST(s.a^) THEN
WITH temp = NEW(REF ARRAY OF Elem.T, 2 * NUMBER(s.a^)) DO
FOR i := 0 TO LAST(s.a^) DO
temp[i] := s.a[i]
END;
s.a := temp
END
END;
s.a[s.n] := x;
INC(s.n)
END Push;
PROCEDURE Pop(VAR s: T): Elem.T =
BEGIN
DEC(s.n);
RETURN s.a[s.n]
END Pop;
BEGIN
END Stack.
然后使用接口例如IntegerElem
,对其进行实例化,只要这个接口定义了泛型模块所需的属性即可:
INTERFACE IntegerElem;
TYPE T = INTEGER;
END IntegerElem.
INTERFACE IntStack = Stack(IntegerElem) END IntStack.
MODULE IntStack = Stack(IntegerElem) END IntStack.
面向对象
Modula-3下的面向对象编程,经常采用部份不透明类型,它通过如下惯用法来声明:Type T <: Public; Public = OBJECT ... END
,这里的不透明类型T
,支持在随后定义的对象类型中的字段和方法,而不会披露T
的确切结构,也不披露T
可能支持的其他方法。
下面的例子,定义一个接口Person
,具有按习惯约定命名的两个类型,导出类型T
,和公开对象类型Public
,这里声明了T
是Public
的不透明子类型,Public
具有两个方法getAge()
和init()
;随后是Person
接口的完全实现:
INTERFACE Person;
TYPE T <: Public;
Public = OBJECT
METHODS
getAge(): INTEGER;
init(name: TEXT; age: INTEGER): T;
END;
END Person.
MODULE Person;
REVEAL T = Public BRANDED OBJECT
name: TEXT;
age: INTEGER;
OVERRIDES
getAge := Age;
init := Init;
END;
PROCEDURE Age(self: T): INTEGER =
BEGIN
RETURN self.age;
END Age;
PROCEDURE Init(self: T; name: TEXT; age: INTEGER): T =
BEGIN
self.name := name;
self.age := age;
RETURN self;
END Init;
BEGIN
END Person.
要建立一个新的Person.T
对象,要使用内建操作NEW()
,并调用方法init()
:
VAR jim := NEW(Person.T).init("Jim", 25);
下面是整数堆栈的例子:
INTERFACE IntegerStack;
TYPE
T <: Public OBJECT;
Public = OBJECT
METHODS
init(): T;
push(x: INTEGER);
pop(): INTEGER;
END;
END IntegerStack.
MODULE IntegerStack;
REVEAL T = Public BRANDED OBJECT
n: INTEGER;
a: REF ARRAY OF INTEGER;
OVERRIDES
init := Init;
push := Push;
pop := Pop;
END;
PROCEDURE Init(self: T): T =
BEGIN
self.n := 0;
self.a := NIL;
RETURN self
END Init;
PROCEDURE Push(self: T; x: INTEGER) =
BEGIN
IF self.a = NIL THEN
self.a := NEW(REF ARRAY OF INTEGER, 5)
ELSIF self.n > LAST(self.a^) THEN
WITH temp = NEW(REF ARRAY OF INTEGER, 2 * NUMBER(self.a^)) DO
FOR i := 0 TO LAST(self.a^) DO
temp[i] := self.a[i]
END;
self.a := temp
END
END;
self.a[self.n] := x;
INC(self.n)
END Push;
PROCEDURE Pop(self: T): INTEGER =
BEGIN
DEC(self.n);
RETURN self.a[self.n]
END Pop;
BEGIN
END IntegerStack.
创建新堆栈在这里的部份不透明类型实现下为:VAR myStack := NEW(IntegerStack.T).init()
,在上述的不透明类型实现下为:VAR myStack := IntStack.Create()
。而压入一个数在这里的部份不透明类型实现下为:myStack.push(8)
,在上述的不透明类型实现下为:IntStack.Push(myStack, 8)
。
多线程
语言支持多线程和在线程间的同步。在运行时库(m3core)中有一个必要接口叫做Thread
,它支持采用分叉会合模型的多线程应用。这里还预定义不透明类型MUTEX
被用来同步多个线程,并保护共享数据免于具有可能损害或竞争条件的同时访问。MUTEX
是一个对象,因此可以从它派生其他对象。
LOCK
语句有如下形式:LOCK mu DO S END
,这里的S
是一个语句,而mu
是一个表达式,它等价于:
WITH m = mu DO
Thread.Acquire(m);
TRY S FINALLY Thread.Release(m) END
END
这里的m
表示不出现在S
中的一个变量。LOCK
语句介入一个互斥锁要锁定的块,并暗含着在代码执行轨迹离开这个块时的解锁它。
不安全标记
某些功能被认为是不安全的,编译器无法再保证结果是一致性的(例如,当与C编程语言交接时)。在INTERFACE
或MODULE
前面加上前缀关键字UNSAFE
,可用于告诉编译器启用语言的某些不安全的低级功能。例如,在UNSAFE
模块中,使用LOOPHOLE()
将整数的诸位元复制成浮点REAL
数,使用DISPOSE()
释放无跟踪的内存。
一个接口是内在安全的,如果在安全模块中使用这个接口,无法产生不核查的运行时间错误。如果导出一个安全接口的所有的模块都是安全的,编译器保证这个接口的内在安全性。如果导出一个安全接口的任何模块是不安全的,就要编程者而非编译器,去做这种保证了。安全接口导入不安全接口,或安全模块导入或导出不安全接口,都是静态错误。导入不安全接口的编译单元本身必定也是不安全的。
对其他编程语言的影响
尽管Modula-3没有获得主流地位,DEC-SRC M3发行的某些部份做到了。可能最有影响的一部份是网络对象库,它形成了Java包括了网络协议的最初的远程方法调用(RMI)实现的基础。在Sun从通用对象请求代理架构(CORBA)标准转移到基于IIOP的协议的时候才被放弃。Java关于远程对象的垃圾收集的文档仍提及了Modula-3网络对象为其先驱性工作[22] 。
Python从Modula-3借鉴了模块系统、例外系统和关键字参数,Python的类机制受到C++和Modula-3的启发[23]。Nim利用了Modula-3的某些方面比如有跟踪和无跟踪的指针。
引用
- ^ Modula-3 Reference Manual (页面存档备份,存于互联网档案馆) Luca Cardelli, James Donahue, Lucille Glassman, Mick Jordan, Bill Kalsow, Greg Nelson. DEC Systems Research Center (SRC) (February 1995)
- ^ Critical Mass Modula-3 (CM3). Critical Mass Modula-3. elego Software Solutions GmbH. [2020-03-21]. (原始内容存档于2020-09-24).
- ^ Polytechnique Montréal Modula-3 (PM3): What is it. Polytechnique Montréal Modula-3. elego Software Solutions GmbH. [2020-03-21]. (原始内容存档于2020-11-19).
- ^ Polstra, John D. Ezm3: An Easier Modula-3 Distribution. CVSup.org. November 9, 2006 [2020-03-21]. (原始内容存档于2007-04-29).
- ^ Weich, Carsten. M3/PC Klagenfurt 96: a Modula-3 environment for MS-DOS. Department of Informatics. University of Klagenfurt. [2020-03-21]. (原始内容存档于2000-05-20).
- ^ Picheta, Dominik; Locurcio, Hugo. Frequently Asked Questions. [2020-03-21]. (原始内容存档于2016-12-10).
- ^ van Rossum, Guido. Programming Python: Foreword (1st ed.). Python.org. May 1996 [2020-03-21]. (原始内容存档于2014-07-24).
besides ABC, my main influence was Modula-3. This is another language with remarkable elegance and power, designed by a small, strong-willed team (most of whom I had met during a summer internship at DEC's Systems Research Center in Palo Alto).
- ^ Baby Modula-3 and a theory of objects (页面存档备份,存于互联网档案馆) Martin Abadi. Digital Equipment Corporation (DEC) Systems Research Center (SRC) Research Report 95 (February 1993).
- ^ Design and History FAQ - Why must ‘self’ be used explicitly in method definitions and calls?. Python.org. March 21, 2020 [2020-03-21]. (原始内容存档于2012-10-24).
The idea was borrowed from Modula-3. It turns out to be very useful, for a variety of reasons.
- ^ Modula-3: Language definition.. [2021-06-25]. (原始内容存档于2021-06-25).
- ^ Rovner, Paul; Levin, Roy; Wick, John. SRC-RR-3 On extending Modula-2 for building large, integrated systems. Hewlett-Packard Labs (报告). January 11, 1985 [2021-08-11]. (原始内容存档于2021-05-16).
- ^ Thacker, Charles P.; Stewart, Lawrence C.; Satterthwaite, Edwin H. Jr. Firefly: a multiprocessor workstation SRC-RR-23. Hewlett-Packard Labs (报告). December 30, 1987 [2021-08-11]. (原始内容存档于2021-05-16).
- ^ McJones, Paul R.; Swart, Garret F. Evolving the Unix system interface to support multithreaded programs SRC-RR-21. Hewlett-Packard Labs (报告). September 28, 1987 [2021-08-11]. (原始内容存档于2021-05-16).
- ^ 14.0 14.1 Modula-3 report (revised) (页面存档备份,存于互联网档案馆) Luca Cardelli, James Donahue, Lucille Glassman, Mick Jordan, Bill Kalsow, Greg Nelson. DEC Systems Research Center (SRC) Research Report 52 (November 1989)
- ^ Peter Robinson. From ML to C via Modula-3 - an approach to teaching programming (PDF). 1994 [2021-08-11]. (原始内容 (PDF)存档于2021-12-17).
The Computer Science course at the University of Cambridge teaches ML as an introductory language at the beginning of the freshman year, and then uses Modula-3 to introduce imperative programming at the end of that year. Further lectures on advanced features of Modula-3 are given early in the second year, together with separate lectures on C. Other, specialised languages are introduced subsequently as the course progresses.
- ^ SPIN OVERVIEW. [2021-08-11]. (原始内容存档于2022-02-24).
SPIN and its extensions are written in Modula-3, a type-safe programming language developed at DEC SRC. Modula-3 offers modern language features such as objects, garbage collection, and threads. We rely on its type-safe properties to protect sensitive kernel data and interfaces from malicious or errant extensions.
- ^ Systems Programming with Modula-3.
- ^ Modula-2 Reference - Statements. [2023-02-21]. (原始内容存档于2023-02-21).
- ^ 19.0 19.1 Luca Cardelli, Jim Donahue, Mick Jordan, Bill Kalsow, Greg Nelson. The Modula-3 Type System (PDF). 1989 [2021-08-12]. (原始内容 (PDF)存档于2021-03-24).
- ^ Why was Python created in the first place?. [2021-06-22]. (原始内容存档于2008-02-23).
I had some experience with using Modula-2+ and talked with the designers of Modula-3 and read the Modula-3 report. Modula-3 is the origin of the syntax and semantics used for exceptions, and some other Python features.
- ^ Scala Center at EPFL.. [2022-05-15]. (原始内容存档于2020-09-23).
- ^ Garbage Collection of Remote Objects (页面存档备份,存于互联网档案馆), Java Remote Method Invocation Documentation for Java SE 8.
- ^ The Python Tutorial - Classes. [2018-07-01]. (原始内容存档于2020-12-03).
It is a mixture of the class mechanisms found in C++ and Modula-3. ……As in Modula-3, there are no shorthands for referencing the object’s members from its methods: the method function is declared with an explicit first argument representing the object, which is provided implicitly by the call. ……I would use Modula-3 terms, since its object-oriented semantics are closer to those of Python than C++, but I expect that few readers have heard of it.
外部链接