| 编程范型 | 指令式,过程式,结构化,模块化,并发 |
|---|---|
| 语言家族 | WirthModula |
| 設計者 | Luca Cardelli(英语:Luca Cardelli), James Donahue, Lucille Glassman, Mick Jordan, Bill Kalsow,Greg Nelson(英语:Greg Nelson (computer scientist))[1] |
| 實作者 | DEC,Olivetti, elego Software Solutions GmbH |
| 发行时间 | 1988年,38年前(1988) |
| 型態系統 | 强类型,静态, 安全或在不安全时显式安全隔离 |
| 作用域 | 词法 |
| 系统平台 | IA-32,x86-64,PowerPC,SPARC |
| 操作系统 | 跨平台:FreeBSD,Linux,Darwin,SunOS |
| 網站 | www |
| 主要實作產品 | |
| SRC(英语:DEC Systems Research Center) Modula-3, CM3[2], PM3[3], EZM3[4], M3/PC Klagenfurt[5] | |
| 受影响于 | |
| ALGOL,Euclid(英语:Euclid (programming language)),Mesa,Modula-2,Modula-2+(英语:Modula-2+),Oberon,Object Pascal | |
| 影響語言 | |
| C#,Java,Nim[6],Python[7],Baby Modula-3(英语:Baby Modula-3)[8] | |
Modula-3是一种系统编程语言,它是叫做Modula-2+(英语:Modula-2+)的升级版本的Modula-2的后继者。虽然它已经在研究界有了影响力,受其影响的语言有Java、C#和Python[9],但未能在工业上被广泛采用。它是在1980年代末由在数字设备公司(DEC)系统研究中心(英语:DEC Systems Research Center)(SRC)和Olivetti研究中心(ORC)的Luca Cardelli(英语:Luca Cardelli)、James Donahue、Lucille Glassman、Mick Jordan(之前在Olivetti软件技术实验室工作)、Bill Kalsow和Greg Nelson(英语:Greg Nelson (computer scientist))设计。
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(英语:DEC Systems Research Center)和Acorn计算机研究中心(ARC,在Olivetti收购Acorn之后叫做ORC)在Modula-2+(英语:Modula-2+)语言上的工作的很大影响[11],Modula-2+(英语:Modula-2+)是编写DEC Firefly(英语:DEC Firefly)多处理器VAX工作站的操作系统的语言[12][13],还是在基于ARM的Acorn Archimedes(英语:Acorn Archimedes)系列计算机的ARX操作系统(英语:ARX (operating system))项目中,ARC编写用于Acorn C和模块执行库(CAMEL)的Acorn编译器的语言[14]。正如修订的Modula-3报告所述[14],该语言演化自Mesa、Modula-2、Cedar和Modula-2+(英语:Modula-2+),还受到其他语言的影响如:Object Pascal,Oberon和Euclid(英语:Euclid (programming language))。
在20世纪90年代,Modula-3作为一种教学语言获得了可观的传播[15],华盛顿大学在1994年发行的微内核操作系统SPIN(英语:SPIN (operating system))是用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程序:
MODULEMain;IMPORTIO;BEGINIO.Put("Hello World\n")ENDMain.
与其他语言一样,Modula-3程序必须有导出Main接口的实现模块,如上述示例中实现模块缺省导出了同名的接口,它可以处在名为Main.m3的文件中;或者是通过EXPORTS导出Main接口的实现模块,比如:
MODULEHelloWorldEXPORTSMain;IMPORTIO;BEGINIO.Put("Hello World\n")ENDHelloWorld.
建议这个模块所在的文件的名字与实际模块名字相同,当它们不同时编译器只是发出一个警告。
Modula-3委员会将Modula-3设计为ALGOL语言家族的现代代表,他们认为:
ALGOL家族语言都有强类型系统,其基本想法是把值空间划分成类型,限制变量持有单一类型的值,并且限制运算应用于固定类型的运算数。这个语言家族的趋势,在1960年代是朝向控制流程和数据结构特征,在1970年代是朝向信息隐藏(英语:Information hiding)特征如接口、不透明类型和泛型,在1980年代是采纳来自LISP家族和始自BCPL的C家族的仔细挑选的技术。
Modula-3委员会坚持一个自称为“复杂度预算”的设计原则:在五十页内将语言准确而易读的描述出来,为此只容纳最有用的特征。精选出来的特征直接针对两个主要目标:
这些特征中任何一个都没有特殊的新颖性,但是组合起来是简单而强力的。人们越是更好的理解程序,就越是使用更大的建造块去构造它。指令之后是语句,语句之后是过程,过程之后是接口,下一步应当是抽象类型。在理论层面,抽象类型,是通过其操作的规定,而非通过其数据的表示,来定义的类型。
关键字:
|
|
|
|
|
|
保留标识符:
|
|
|
|
|
|
|
运算符:
+ | < | # | = | ; | .. | : |
- | > | { | } | | | := | <: |
* | <= | ( | ) | ^ | , | => |
/ | >= | [ | ] | . | & |
此外,注释是开于(*并合于*)的任意字符的序列。注释可以嵌套并可以扩展跨越多于一行。
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]。一个块有如下形式:
DeclsBEGINSEND
这里的Decls是成序列的声明,而S是一个语句,经常是顺序复合语句即成序列的语句,它是这个块的执行部份。这里的Decls与S在缩进上对齐,是因为在Pascal语言家族中,可能含有嵌套过程声明的声明序列的位置相较于ALGOL 60,从紧随在关键字BEGIN之后转移到了紧靠在其前面。一个块的声明可以介入一个名字最多一次,然而一个名字可以在嵌套的块中重新声明。在块中这些声明的次序,除了确定变量的初始化次序之外无关紧要。
一个Modula-3程序规定了一个计算,它作用在一序列的叫做地点(location)的数字元件之上。变量是地点的一个集合,它依据这个变量的类型所确定的约定,表示一个数学上的值(英语:Value (mathematics))。如果一个值可以由类型T的某个变量来表示,则称谓这个值是T的一个成员,并且T包含这个值。
声明为一个变量、类型、过程、常量(英语:Constant (computer programming))或例外指定一个被称为标识符的符号(英语:Symbol (programming))作为名字。一个声明应用于其上的程序区域,叫做这个声明的作用域。作用域是可以嵌套的。一个标识符的含义,由这个标识符在其中声明的最小的包围作用域来确定。
类型声明有如下形式:TYPET=U,这里的T是一个标识符,而U是一个类型或类型表达式。在需要类型的各种地方,通常允许类型表达式。
变量声明有如下形式:VARid:T:=E,这里的id是一个标识符,T是除了开放数组之外的一个非空类型,而E是一个表达式。它声明了id是类型T的一个变量,其初始值是E的值。:= E和: T任何一个都可省略,但二者不能都省略。如果省略了T,它采用E的类型。如果省略了E,初始值是类型T的任意值。如果二者都存在,E必须可赋值给T。初始值是一种简写,等价于在随后的块的可执行部份开始处插入赋值id := E,如果多个变量有初始值,按它们声明的次序插入它们的赋值。形式VARv_1,...,v_n:T:=E,是VARv_1:T:=E;...;VARv_n:T:=E的简写。
常量(英语:Constant (computer programming))声明有如下形式:CONSTid:T=C,这里的id是一个标识符,T是一个类型,而C是一个常量表达式。它声明id为具有类型T和值C的一个常量。: T可以省略,在这种情况下id的类型是C的类型。如果T存在,则它必须包含C。
Modula-3的类型系统使用结构等价(英语:Structural type system),而非Modula-2的名称等价(英语:Nominal type system)即类型构造子的每次出现产生一个新类型。在结构等价中,在如果两个类型的定义在展开的时候,也就是在所有常量表达式都被替换为它们的值,并且所有类型名字都被替代为它们的定义的时候,变成相同的,则这两个类型是相同的。在递归数据类型的情况下,这个展开是部份展开的无穷极限。
有三种序数类型:枚举、子范围和整数类型,预定义了如下序数类型: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之外的引用类型。两个引用如果寻址相同的地点则二者相等。
记录是叫做这个记录的字段的命名变量的一个序列,记录类型的声明有如下形式:TYPET=RECORDFieldListEND,在这里的类型表达式中,算子(英语:Operator (computer programming))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之中。
数组是叫做这个数组的元素的组成变量的有索引的搜集。有两种数组类型,固定的和开放的。固定数组的长度是在编译时间确定的,开放数组的长度是在运行时间分配确定的,这个长度此后不能改变。两个数组如果有相同的长度,并且对应的元素都相等则二者相等。
压缩类型的变量出现在记录、对象或数组中,占据指定数量的位元,并毗邻于前导的字段或元素紧压打包。压缩类型的声明有如下形式:TYPET=BITSnFORBase,这里的Base是一个基础类型,而n是整数取值的常量表达式。
集合是取自某个序数类型的值的搜集。集合类型声明有如下形式:TYPET=SETOFBase,这里的基础类型Base是一个序数类型。两个集合如果有相同的元素则二者相等。
不包含值的一个类型是空的,例如[1..0]是一个空类型。空类型可以用来建造非空类型,比如SET OF [1..0]。声明一个空类型的一个变量是静态错误。
所有的表达式(英语:Expression (computer science))都有一个唯一的类型,但是一个值可以是很多类型的一个成员,例如值6是[0..9]和INTEGER二者的成员,因此论述“一个值的类型”将会是有歧义的。短语“x的类型”,意味着“表达式x的类型”;然而短语“x是T的成员”,意味着“x的值是T的成员”。但在有一种情况下,一个值可以称为有着一个类型:所有的对象或有跟踪的引用值,包括了一个类型代码,它叫做这个引用值的分配类型。分配类型通过TYPECASE语句来测试。
语言规定提供了类型运算BITSIZE()、BYTESIZE()和ADRSIZE(),分别返回变量x或者类型T的那些变量的位元数目、8位字节的数目和可寻址地点的数目。在所有情况下,x必须是指定式,而T必须不是开放数组类型,指定式x只在它的类型是开放数组的时候求值。
枚举类型声明有如下形式:TYPET={id_1,id_2,...,id_n},这里的id是各不相同的标识符,类型T是n个值的有序集合,表达式T.id_i指示这个类型在递增次序下的第i个值,空枚举{ }是允许的。
子范围类型声明有如下形式:TYPET=[Lo..Hi],这里的Lo和Hi是有相同的基础类型的两个序数值,T的值是从Lo到Hi含二者所有的值,它们必须是常量表达式,如果Lo超过Hi,则子范围是空的。
每个不同的枚举类型介入新的一组值,而子范围类型重复使用底层类型的值。例如:
TYPET1={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。
固定数组类型声明有如下形式:TYPET=ARRAYIndexOFElement,这里的索引类型Index是序数类型,而元素类型Element是除了开放数组之外的任何类型。开放数组类型声明有如下形式:TYPET=ARRAYOFElement,这里的元素类型Element是任何类型。
数组类型T的值,是元素类型为Element的数组;对于固定数组类型,这个数组的长度是索引类型Index的元素数目;对于开放数组类型,这个数组的长度是任意的;开放数组的索引类型是INTEGER子范围[0..n-1],这里的n是这个数组的长度。开放数组类型只能用作形式参数的类型,引用类型的所引用者,另一个开放数组的元素类型,或作为数组构造子中的类型。
多维数组的形状是它每一维的长度的序列。更精确的说,一个数组的形状是它的长度跟随着任何它的元素的形状,非数组的形状是空序列。数组如果有相同的类型和形状,则它们是可赋值的。如果赋值的要么来源要么目标是开放数组,则需要运行时间检查。形如ARRAYIndex_1,...,Index_nOFElement的表达式,是ARRAYIndex_1OF...OFARRAYIndex_nOFElement的简写。
如果a有数组类型T,则a[i]指示a的一个指定元素,它在数组中的位置对应于i在索引类型中位置。形如a[i_1, ..., i_n]的表达式,是a[i_1]...[i_n]的简写。索引的解释由一个数组的类型确定,而非它的值;数组赋值改变数组变量的值,而非它的类型。例如:
VARa:=ARRAY[1..3]OFREAL{1.0,2.0,3.0};b:ARRAY[-1..1]OFREAL:=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只在它指示一个开放数组的时候求值。
一个引用类型要么是有跟踪的要么是无跟踪的。当到已分配的一段存储的所有的有跟踪的引用都消失时,系统对这段存储进行垃圾回收。有跟踪的引用类型以如下形式来声明:TYPET=REFType,这里的Type是任何类型。T的值是到类型Type的变量的有跟踪的引用,Type叫做T的所引用类型。无跟踪的引用类型以如下形式来声明:TYPET=UNTRACEDREFType,这里的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。
表达式(英语:Expression (computer science))规定产生一个值或变量的一个计算。所有的表达式都有一个静态确定的类型,它包含这个表达式可以产生的所有的值。在语法上,一个表达式要么是一个运算数,要么是应用于自身也是表达式的实际参数的一个运算。运算数是标识符、文字(英语:Literal (computer programming))或类型。通过递归的求值一个运算的实际参数并应用这个运算,来求值一个表达式。实际参数的求值次序,对除了短路求值的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。
表达式可建构自如下文字(英语:Literal (computer programming)):整数文字(英语:Integer literal)、实数文字、字符文字(英语:Character literal)和文本文字(英语:String literal)。集合、数组和记录有构造子表达式:
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的一个值,它的字段值由重写后的这些绑定来指定。常量(英语:Constant (computer programming))表达式是一般性的表达式类的子集,所要求的限制使得可以静态的求值这个表达式。在常量表达式中,除了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^,这个运算叫做解引用(英语:Dereference operator)(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<:REFT<:REFANYNULL<:UNTRACEDREFT<:ADDRESS
就是说,REFANY和ADDRESS分别包含所有的有跟踪的和无跟踪的引用,NIL是所有的引用类型的一个成员。这两个规则也适用于加铭牌的类型。
对于对象类型有:
ROOT<:REFANYUNTRACEDROOT<:ADDRESSNULL<:TOBJECT...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+(英语: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允许这两种风格,语言报告将其用作分隔符。
块语句有如下形式:DeclsBEGINSEND,这里的Decls是一序列的声明,S是一个语句。块介入在Decls中声明的常量、类型、变量和过程,并接着执行S。声明的名字的作用域是这个块。
Modula-3提供了选择控制结构IF和CASE语句。IF语句有如下形式:
IFB_1THENS_1ELSIFB_2THENS_2...ELSIFB_nTHENS_nELSES_0END
这里的这些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的同名语句,它有如下形式:
CASEExprOFL_1=>S_1|...|L_n=>S_nELSES_0END
这里的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语句有如下形式:
TYPECASEExprOFT_1(v_1)=>S_1|...|T_n(v_n)=>S_nELSES_0END
这里的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语句。语句LOOPSEND,循环直到S中有一个EXIT发生。语句WHILEBDOSEND,被定义为:
LOOPIFBTHENSELSEEXITENDEND
语句REPEATSUNTILB,被定义为:
LOOPS;IFBTHENEXITENDEND
FOR语句有如下形式:FORid:=firstTOlastBYstepDOSEND,这里的id是一个标识符,而first和last是有相同基础类型的序数表达式,step是整数取值的表达式。而S是一个语句。BY step是可选的,如果省略,step缺省为1。标识符id指示其作用域是S的一个只读变量,其类型是first和last共同的基础类型。FOR语句的语义,可如下这样用WHILE和WITH语句精确的定义为一个块:
VARi:=ORD(first);done:=ORD(last);delta:=step;BEGINIFdelta>=0THENWHILEi<=doneDOWITHid=VAL(i,T)DOSEND;INC(i,delta)ENDELSEWHILEi>=doneDOWITHid=VAL(i,T)DOSEND;INC(i,delta)ENDENDEND
这里的T是id的类型,i、done和delta代表不出现在FOR语句中的变量。从FOR语句的这个定义可以看出,如果一个FOR循环的上界是LAST(INTEGER)或LAST(LONGINT),就应当将它改写为WHILE循环来避免溢出。
Modula-3的WITH语句,无关于Modula-2的同名语句,它有如下形式:WITHid=eDOSEND,这里的id是一个标识符,e是一个表达式,而S是一个语句。WITH语句声明了具有作用域S的id,作为变量e的别名,或作为值e的只读名字。表达式e在进入WITH语句的时候被求值一次。一个单一的WITH语句可以包含多个绑定,它们顺序的求值,就是说WITHid_1=e_1,id_2=e_2,...,等价于WITHid_1=e_1DOWITHid_2=e_2DO....。
WITH语句可类比于某种过程调用P(e),这里的P被声明为:
PROCEDUREP(modeid:typeofe)=BEGINSENDP
这里的算符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,这两个语句分别等价于WITHx=vDOx:=VAL(ORD(x)+n,T)END和WITHx=vDOx:=VAL(ORD(x)-n,T)END,这里的T是v的类型,而x代表不出现在n中的变量。
例外声明有如下形式:EXCEPTION id(T),这里的id是一个标识符,而T是除了开放数组之外的一个类型,它声明了id是具有实际参数类型T的例外。如果省略了(T),这个例外不接受实际参数。例外声明只允许出现在接口和模块的最外层作用域之中。所有的声明的例外都是各不相同的。
RAISE语句有两种形式,没有实际参数的形式:RAISEe,这里的e是不接受实际参数的一个例外,它的结果是发起例外e;具有实际参数的形式:RAISEe(x),这里的e是接受实际参数的一个例外,而x是可赋值给e的实际参数类型的一个表达式,它的结果是引发具有配对实际参数x的例外e。
Modula-3的例外处理,基于了TRY-EXCEPT语句。采用类似的块系统的有Delphi、Python[20]、Scala[21]和Visual Basic.NET。TRY-EXCEPT语句有如下形式:
TRYBodyEXCEPTid_1(v_1)=>Handler_1|...|id_n(v_n)=>Handler_nELSEHandler_0END
这里的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语句,它有如下形式:TRYS_1FINALLYS_2END,它执行语句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接受任意类型的实际参数,程序不能显式的命名这些例外。语句LOOPSEND,非正式的等价于:
TRYS;S;S;...EXCEPTexit-exception=>(*skip*)END
(formal_1; ...; formal_n): R RAISES S,这里的每个formal_i是形式参数,R是结果类型,S是RAISES集合(这个过程可以引发的例外的集合)。形式参数有如下形式:ModeName:Type:=Default,这里的:
Mode是参数模态,它可以是VALUE、VAR或READONLY。如果省略了Mode,它缺省为VALUE。Name是命名这个形式参数的标识符。形式参数名字必须是各不相同的。Type是这个形式参数的类型。Default是一个常量表达式,它是这个形式参数的缺省值。如果Mode是VAR,则:= Default必须省略,否则:= Default和: Type中任何一个都可以省略,但不能二者都省略。如果省略了Type,它采用Default的类型。如果二者都存在,Default的值必须是Type的一个成员。形式Modev_1,...,v_n:Type:=Default,是Modev_1:Type:=Default;...;Modev_n:Type:=Default的简写。
VAR形式参数绑定到对应的实际参数所指示的变量上,也就是说它是别名。对于VAR形式参数,实际参数必须是可写的指定式,它的类型同于形式参数的类型,或者在VAR数组形式参数的情况下,它可赋值给这个形式参数。
VALUE形式参数绑定到具有未用的位置的一个变量上,并且它被初始化为实际参数对应的值。对于VALUE或READONLY形式参数,实际参数是可赋值给形式参数类型的任何表达式,并且放开了针对赋值局部过程的禁止。
READONLY形式参数,如果实际参数是指定式并且有与形式参数相同的类型,或者是可赋值给形式参数的类型的一个数组类型,则被当作VAR形式参数对待,它的只读语义体现在指定这个指定式为只读的,否则被当作VALUE形式参数对待。
在签名中,如果省略了: R,则这个过程是真正(proper)过程,否则为函数过程;如果省略了RAISES S,则假定它为RAISES {}。一个过程发起未包括在RAISES集合中的一个例外,是一个必查的运行时间错误,如果语言实现将这个运行时间错误映射成一个例外,则这个例外隐含的包括在所有的RAISES子句中。
有两种形式的过程声明:只允许出现在接口中的PROCEDUREidsig,和只允许出现在模块中的PROCEDUREidsig=Bid,这里的id是一个标识符,sig是过程签名,而B是一个块。在一个模块的最外层作用域内声明的过程是顶层过程,其他过程是局部过程。局部过程可以作为形式参数传递但不能被赋值,因为在栈实现下,局部过程在包含它的栈桢被弹出后会成为无效的。
过程类型声明有如下形式:TYPET=PROCEDUREsig,这里的sig是签名规定。一个过程P是过程类型T的一个成员,或称为是它的一个值的条件为,它是NIL,或者它的签名被T的签名所含盖(cover)。这里的签名sig_A含盖签名sig_B的条件是:
sig_A的RAISES集合包含sig_B的RAISES集合。过程常量是声明为过程的一个标识符。过程变量是声明为具有过程类型的一个变量。例如:
PROCEDUREP(txt:TEXT:="P")=BEGINIO.Put(txt)ENDP;VARq: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的真正过程,在绑定了实际参数之后等价于:
TRYBEXCEPTreturn-exception=>(*skip*)END
而调用具有过程体B的函数过程等价于:
TRYB;(错误:没有返回值)EXCEPTreturn-exception(v)=>(结果成为v)END
对于常量、类型或过程声明N = E、变量声明N: E、例外声明N(E)或揭示N = E,如果N出现在E的任何部份展开之中,则它是递归的。对于省略了类型的变量声明N := I,如果N出现在I的类型E的任何部份展开中,则它是递归的。允许这种声明的条件,是N在E的任何部份展开中所有的出现为如下三者之一:在类型构造子REF或PROCEDURE的某个出现之内,在类型构造子OBJECT的一个字段或方法类型之内,或者在一个过程体之内。下面是合法的递归声明的例子:
TYPERealList=REFRECORDval:REAL;link:RealListEND;ListNode=OBJECTlink:ListNodeEND;IntList=ListNodeOBJECTval:INTEGEREND;T=PROCEDURE(n:INTEGER;p:T);EXCEPTIONE(PROCEDURE()RAISES{E});PROCEDUREP(b:BOOLEAN)=BEGINIFbTHENP(NOTb)ENDENDP;
Modula-3的设计者认为Modula-2最成功的特征,是提供了在模块之间的显式接口,故而接口在Modula-3中保留而在本质上没有变化。到模块的接口,是披露了模块公开部份的声明搜集,而未声明于接口中的模块中的东西是私有的。模块导入它所依赖的接口,而导出它所实现的接口(在Modula-3中可以是多个接口)。
在现代编程语言的实现下,一个抽象类型的值,被表示一个对象,它的操作由叫做对象的方法的过程值的一个套件来实现。一个新的对象类型,可以定义为现存类型的子类型,在这种情况下新类型拥有旧类型的所有方法,并且可能还有新的方法(继承)。新类型可以为旧方法提供新的实现(覆盖)。对象类型和Modula-2的不透明类型的组合,产生了一个新事物:部份不透明(partially opaque)类型,这里的一个对象的一些字段在一个作用域内是可见的,而其他的是隐藏的。
泛型模块是一种模板,在其中一些导入的接口被当作形式参数,它们在实例化泛型的时候要绑定到实际接口。不同的泛型实例是独立编译的,源代码程序被重复使用,而编译后的代码对不同实例通常是不同的。为保持简单性,在Modula-3中泛型被限制在模块层面,泛型的过程和类型不孤立存在,并且泛型形式参数必须都是完全的接口。
将计算分解入并发进程(或线程)中是关注点分离的基本方法。Modula-2提供的线程很弱,本质上相当于协程。Modula-3采用了霍尔的监视器,它是并发编程的更坚实基础。Modula-2+(英语:Modula-2+)在设计中加入了简化的Mesa同步原语,它足够成功而在实质上没有改变的结合入了Modula-3。
一个语言特征是不安全的,如果它的滥用可以破坏运行时系统,使得程序的进一步运行不忠实于语言的语义,不安全特征的一个例子,是没有边界检查的数组赋值。一个安全的程序的一个错误,可以导致计算中止,并给出运行时间错误信息或给出错误解答,但它不会导致计算分崩离析。Modula-3在这个领域里跟从Cedar,接受只在显式的标记为不安全的模块中才允许的少量不安全特征。
接口中的声明,除了任何变量初始化必须是常量,和过程声明必须只能指定签名而不能有过程体之外,同于在块中的声明。接口有如下形式:
INTERFACEid;Imports;DeclsENDid.
这里的id是命名这个接口的标识符,Imports是一序列的导入语句,而Decls是一序列的声明。在Decls中的名字和可见的导入的名字必须是各不相同的。两个或更多个接口形成导入环是静态错误。典型的接口定义,通常为每个接口定义一个数据结构(记录或对象)以及任何的支持过程。
模块除了实体名字的可见性之外,就像是一个块。一个实体在一个块中是可见的,如果它被声明在这个块中,或在某个包围的块中;而一个实体在一个模块中是可见的,如果它被声明在这个模块中,或在这个模块所导入或导出的一个接口中。模块有如下形式:
MODULEidEXPORTSInterfaces;Imports;Blockid.
这里的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语句有如下的典型使用形式:IMPORTI,它导入全局名字为I的接口,并给它一个同样的局部名字,从而使用点表示法访问接口中的实体(类似于访问记录中的字段)。比如接口中的主要类型按惯例命名为T,则使用它时采用的限定形式为I.T。如果导入接口的名字与模块内的其他实体的名字发生冲突,则可以使用IMPORTIASX这样的形式给与它不同的局部名字。
IMPORT语句还有如下的另一种形式:FROMIIMPORTN,它为在接口I中声明为N的实体,介入一个局部名字N,比如:FROMIOIMPORTPut,并且不加限定的使用它,比如:Put("Hello World\n")。局部绑定优先于全局绑定,例如:IMPORTIASJ,JASI;FROMIIMPORTN,为全局名字为J.N的实体介入了局部名字N。非法使用相同的局部名字两次是静态错误。
一个程序是模块和接口的搜集,包含了任何它的模块或接口所导入或导出的所有接口,并且在其中没有重复定义的过程、模块或接口。执行一个程序的效果是执行它的所有模块的模块体。模块的执行次序受到初始化规则的约束:如果模块M依赖于模块N并且N不依赖于M,则N的模块体将先于M的模块体执行。这里的模块M依赖于模块N,如果M使用了N导出的一个接口,或者M所依赖的一个模块依赖于N。
其模块体最后执行的模块叫做主模块。在初始化规则不能唯一的确定它的情况下,语言实现提供了指定这个主模块的方法,即主模块是导出Main的模块,它的内容是依赖于实现的。程序执行在主模块终止的时候终止,即使并发的控制线程仍在执行。
下面是接口与模块的一个规范性的例子,是有隐藏表示的一个公开堆栈:
INTERFACEStack;TYPET<:REFANY;PROCEDURECreate():T;PROCEDUREPush(VARs:T;x:REAL);PROCEDUREPop(VARs:T):REAL;ENDStack.
MODULEStack;REVEALT=BRANDEDOBJECTitem:REAL;link:TEND;PROCEDURECreate():T=BEGINRETURNNILENDCreate;PROCEDUREPush(VARs:T;x:REAL)=BEGINs:=NEW(T,item:=x,link:=s)ENDPush;PROCEDUREPop(VARs:T):REAL=VARres:REAL;BEGINres:=s.item;s:=s.link;RETURNresENDPop;BEGINENDStack.
如果多于一个模块需要此堆栈的表示,则应当把它转移到私有接口中,从而在需要它的任何地方的都可以导入它:
INTERFACEStack;(* ... 同上 ... *)ENDStack.INTERFACEStackRep;IMPORTStack;REVEALStack.T=BRANDEDOBJECTitem:REAL;link:Stack.TENDENDStackRep.
MODULEStack;IMPORTStackRep;(* Push、Pop和Create同上 *)BEGINENDStack.
一个对象要么是NIL,要么是到有配对的方法套件的一个数据记录的引用,方法套件是接受这个对象作为第一个实际参数的那些过程的一个记录。这个数据记录可以包含经由对象类型的子类型介入的增加字段,这个套件可以包含经由子类型介入的增加方法。对象赋值是引用赋值。要复制一个对象的数据记录到另一个对象,这些字段必须单独的赋值。
Modula-3意图保持采用对象这个最简单的术语,而非在其他面向对象语言中对应的术语类。对象类型声明采用如下形式:
TYPET=STOBJECTFieldsMETHODSMethodsOVERRIDESOverridesEND
这里的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方法所含盖的签名。举例说明:
TYPESuper=OBJECTa:INTEGER;METHODSp()END;Sub=SuperOBJECTb:INTEGEREND;PROCEDUREProcSuper(self:Super)=...;PROCEDUREProcSub(self:Sub)=...;
这里的过程ProcSuper和ProcSub是类型Super和Sub的对象的p方法的候选值。例如:
TYPET1=SubOBJECTOVERRIDESp:=ProcSubEND | TYPET2=SuperOBJECTOVERRIDESp:=ProcSuperEND |
声明了具有Sub数据记录和预期一个Sub的p方法的一个类型。T1是Sub的有效的子类型。 | 声明了具有Super数据记录和预期一个Super的p方法的一个类型。T2是Super的有效的子类型。 |
TYPET3=SubOBJECTOVERRIDESp:=ProcSuperEND | TYPET4=SuperOBJECTOVERRIDESp:=ProcSubEND |
声明了具有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(TOBJECTOVERRIDESm:=PEND)的语法糖。
如果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,如果检查失败则发生运行时间错误。
下面的例子展示了在声明新方法和覆盖现存方法之间的不同,首先声明如下:
TYPESuper=OBJECTMETHODSm():=ProcSuperEND;SubOverridden=SuperOBJECTOVERRIDESm:=ProcSubEND;SubExtended=SuperOBJECTMETHODSm():=ProcSubEND;VARa:=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)。
对象不能解引用(英语:Dereference operator),因为在语言实现中,一个对象变量的静态类型不确定它的数据记录的类型。对象类型确定了关乎数据记录字段和方法套件的超类型链的前缀的那些类型。
下面是语言设计者提出的对象的一种可能实现的梗概[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)类型声明有如下形式:TYPET<:U,这里的T是标识符,而U是指示一个引用类型的表达式。它将名字T介入为不透明类型,并披露了U是T的超类型。
Modula-3的揭示(revelation)机制,将关于一个不透明类型的信息,介入到一个作用域之内,不同于其他声明,揭示不介入新的名字。不同的作用域,可以披露(reveal)关于一个不透明类型的不同的信息。例如,某个不透明类型,在一个作用域内知晓为REFANY的子类型,在其他的作用域内可以知晓为ROOT的子类型。
不透明类型名字所指示的实际类型叫做具体类型。T的具体类型,必须披露于程序的其他地方。在对象声明中,如果ST被声明为不透明类型,则只在知晓ST的具体类型为对象类型的作用域内,T的声明是合法的。如果T被声明为不透明类型,则只在完全知晓T的具体类型,或者知晓它为对象类型的作用域内,NEW(T)是合法的。
揭示分为两种:部份的和完全的,一个程序可以包含一个不透明类型的任意多个部份揭示,但必须包含唯一的一个完全揭示。
部份揭示有如下形式:REVEALT<:V,这里的V是类型表达式(可能就是个名字),而T是被声明为不透明类型的一个标识符(可能有限定)。它披露了V是T的超类型。在任何作用域内,对一个不透明类型披露的超类型,必须是在子类型关系下是线性有序的。就是说,如果披露了T <: U1和T <: U2,则必须也要披露要么U1 <: U2要么U2 <: U1。REVEALT<:T是合法的非递归声明。
完全揭示有如下形式:REVEALT=V,这里的V是类型表达式(不能就是个名字),它的最外层类型构造子,是有铭牌的一个引用或对象类型,而T是被声明为不透明类型的一个标识符(可能有限定)。这个揭示指定了V是T的具体类型。在任何作用域内,披露为T的超类型的任何类型,都必须是V的超类型,否则是一个静态错误。不同的不透明类型有不同的具体类型,因为V包含了一个铭牌并且在程序中所有的铭牌都是独一的。REVEALI.T=I.TBRANDEDOBJECT...END是非法的递归声明。
揭示只允许用在接口和模块的最外层作用域内。在接口中的揭示可以被导入到任何需要它的作用域之内。揭示提供了向客户隐藏实现细节的,在概念上简单清晰却非常强力的机制,例如:
INTERFACEI;TYPET<:ROOT;PROCEDUREP(x:T):T;ENDI.INTERFACEIRep;IMPORTI;REVEALI.T=MUTEXBRANDEDOBJECTcount:INTEGEREND;ENDIRep.INTERFACEIClass;IMPORTI;REVEALI.T<:MUTEX;ENDIClass.
导入I的编译单元见到的I.T是ROOT的不透明子类型,因而被限制为可以分配类型I.T的对象,把它们传递给I.P,或声明I.T的子类型。导入IRep的编译单元见到的I.T是具体类型,它是有扩展的count字段的MUTEX的子类型。导入IClass的编译单元见到的I.T是MUTEX的不透明子类型,因而可以锁定类型I.T的对象。
在泛型接口或模块中,某些导入的接口名字被当作形式参数,它们在泛型被实例化的时候要绑定到实际接口上。泛型接口及其实例有如下左侧的形式,并等价于右侧定义的普通接口:
GENERICINTERFACEG(F_1,...,F_n);BodyENDG.INTERFACEI=G(A_1,...,A_n)ENDI. | INTERFACEI;IMPORTA_1ASF_1,...,A_nASF_n;BodyENDI. |
这里的G是命名这个泛型接口的一个标识符,F_1, ..., F_n是叫做G的形式导入的标识符的一个列表,而Body同在非泛型接口中一样,是一序列导入和随后的一序列声明。这里的I是这个实例的名字,而A_1, ..., A_n是G的形式导入要绑定的实际接口的一个列表。
泛型模块及其实例有如下左侧的形式,并等价于右侧定义的普通模块:
GENERICMODULEG(F_1,...,F_n);BodyENDG.MODULEIEXPORTSE=G(A_1,...,A_n)ENDI. | MODULEIEXPORTSE;IMPORTA_1ASF_1,...,A_nASF_n;BodyENDI. |
这里的G是命名这个泛型模块的一个标识符,F_1, ..., F_n是叫做G的形式导入的标识符的一个列表,而Body同在非泛型模块中一样,是一序列的导入和随后的一个块。这里的I是这个实例的名字,E是由M导出的接口的一个列表,而A_1, ..., A_n是G的形式导入要绑定的实际接口的一个列表。EXPORTS E可以省略,在这种情况下它缺省为EXPORTS I。泛型模块自身没有导出,它们只在被实例化时提供。
泛型接口及其对应的泛型模块(与C++模板一样)可以轻松定义和使用抽象数据类型,但粒度在模块级别,裸露的类型INTEGER或REAL不能使用,相比之下,它们在C++模板中可以使用。例如,可以定义泛型堆栈:
GENERICINTERFACEStack(Elem);(* 这里的Elem.T不是开放数组类型 *)TYPET<:REFANY;PROCEDURECreate():T;PROCEDUREPush(VARs:T;x:Elem.T);PROCEDUREPop(VARs:T):Elem.T;ENDStack.
GENERICMODULEStack(Elem);REVEALT=BRANDEDOBJECTn:INTEGER;a:REFARRAYOFElem.TEND;PROCEDURECreate():T=BEGINRETURNNEW(T,n:=0,a:=NIL)ENDCreate;PROCEDUREPush(VARs:T;x:Elem.T)=BEGINIFs.a=NILTHENs.a:=NEW(REFARRAYOFElem.T,5)ELSIFs.n>LAST(s.a^)THENWITHtemp=NEW(REFARRAYOFElem.T,2*NUMBER(s.a^))DOFORi:=0TOLAST(s.a^)DOtemp[i]:=s.a[i]END;s.a:=tempENDEND;s.a[s.n]:=x;INC(s.n)ENDPush;PROCEDUREPop(VARs:T):Elem.T=BEGINDEC(s.n);RETURNs.a[s.n]ENDPop;BEGINENDStack.
然后使用接口例如IntegerElem,对其进行实例化,只要这个接口定义了泛型模块所需的属性即可:
INTERFACEIntegerElem;TYPET=INTEGER;ENDIntegerElem.INTERFACEIntStack=Stack(IntegerElem)ENDIntStack.
MODULEIntStack=Stack(IntegerElem)ENDIntStack.
Modula-3下的面向对象编程,经常采用部份不透明类型,它通过如下惯用法来声明:TypeT<:Public;Public=OBJECT...END,这里的不透明类型T,支持在随后定义的对象类型中的字段和方法,而不会披露T的确切结构,也不披露T可能支持的其他方法。
下面的例子,定义一个接口Person,具有按习惯约定命名的两个类型,导出类型T,和公开对象类型Public,这里声明了T是Public的不透明子类型,Public具有两个方法getAge()和init();随后是Person接口的完全实现:
INTERFACEPerson;TYPET<:Public;Public=OBJECTMETHODSgetAge():INTEGER;init(name:TEXT;age:INTEGER):T;END;ENDPerson.
MODULEPerson;REVEALT=PublicBRANDEDOBJECTname:TEXT;age:INTEGER;OVERRIDESgetAge:=Age;init:=Init;END;PROCEDUREAge(self:T):INTEGER=BEGINRETURNself.age;ENDAge;PROCEDUREInit(self:T;name:TEXT;age:INTEGER):T=BEGINself.name:=name;self.age:=age;RETURNself;ENDInit;BEGINENDPerson.
要建立一个新的Person.T对象,要使用内建操作NEW(),并调用方法init():
VARjim:=NEW(Person.T).init("Jim",25);
下面是整数堆栈的例子:
INTERFACEIntegerStack;TYPET<:PublicOBJECT;Public=OBJECTMETHODSinit():T;push(x:INTEGER);pop():INTEGER;END;ENDIntegerStack.
MODULEIntegerStack;REVEALT=PublicBRANDEDOBJECTn:INTEGER;a:REFARRAYOFINTEGER;OVERRIDESinit:=Init;push:=Push;pop:=Pop;END;PROCEDUREInit(self:T):T=BEGINself.n:=0;self.a:=NIL;RETURNselfENDInit;PROCEDUREPush(self:T;x:INTEGER)=BEGINIFself.a=NILTHENself.a:=NEW(REFARRAYOFINTEGER,5)ELSIFself.n>LAST(self.a^)THENWITHtemp=NEW(REFARRAYOFINTEGER,2*NUMBER(self.a^))DOFORi:=0TOLAST(self.a^)DOtemp[i]:=self.a[i]END;self.a:=tempENDEND;self.a[self.n]:=x;INC(self.n)ENDPush;PROCEDUREPop(self:T):INTEGER=BEGINDEC(self.n);RETURNself.a[self.n]ENDPop;BEGINENDIntegerStack.
创建新堆栈在这里的部份不透明类型实现下为:VARmyStack:=NEW(IntegerStack.T).init(),在上述的不透明类型实现下为:VARmyStack:=IntStack.Create()。而压入一个数在这里的部份不透明类型实现下为:myStack.push(8),在上述的不透明类型实现下为:IntStack.Push(myStack,8)。
语言支持多线程和在线程间的同步。在运行时库(m3core)中有一个必要接口叫做Thread,它支持采用分叉会合模型的多线程应用。这里还预定义不透明类型MUTEX被用来同步多个线程,并保护共享数据免于具有可能损害或竞争条件的同时访问。MUTEX是一个对象,因此可以从它派生其他对象。
LOCK语句有如下形式:LOCKmuDOSEND,这里的S是一个语句,而mu是一个表达式,它等价于:
WITHm=muDOThread.Acquire(m);TRYSFINALLYThread.Release(m)ENDEND
这里的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的某些方面比如有跟踪和无跟踪的指针。
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).
The idea was borrowed from Modula-3. It turns out to be very useful, for a variety of reasons.
The Computer Science course at theUniversity of Cambridge teachesML 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 onC. Other, specialised languages are introduced subsequently as the course progresses.
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.
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.
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.