Andrew McGettrick 在他的优秀教科书《Algol 68:第一和第二课程》的序言中写道:
“本书最初来源于 1973-74 年在斯特拉斯克莱德大学为一年级本科生开设的讲座,他们中的许多人以前没有编程知识。许多学生并没有将计算机科学作为他们的主修科目,而仅仅是作为一门辅助科目。因此,他们是合适的听众,可以对他们进行讲座,试图教授 Algol 68 作为第一门编程语言。”
也许这段引言对我来说具有特殊的意义,因为我也是 1973-1974 年的一年级学生,尽管是在不同的机构——不列颠哥伦比亚大学。此外,“在那些日子里”,UBC 的计算机科学入门课程是在二年级使用滑铁卢 FORTRAN 和一点 IBM 360 汇编语言教授的;没有 Algol 68 那么 exotic。就我而言,我直到三年级才遇到 Algol 68。也许这种等待,以及其他编程语言的经验,促成了我对这种被低估和美妙的编程语言的终生迷恋。感谢 Marcel van der Veer,他创建了 Algol 68 的一个非常好的实现,名为 Algol 68 Genie,现在它在我的发行版的存储库中,我终于能够悠闲地探索 Algol 68 了。我还应该提到 Marcel 的书,《学习 Algol 68 Genie》,对于新手和 Algol 68 的复习课程都非常有用。
因为我重新发现 Algol 68 非常有趣,所以我认为应该分享一些我的想法和印象。
人们对 Algol 68 的评价
如果阅读 维基百科上关于 Algol 68 的概述值得,那么阅读 Algorithmic Language Algol 68 修订报告 中的这段话就真的值得了
“最初的作者愉快而感谢地承认 WG 2.1 成员和许多其他对 Algol 感兴趣的人的全心全意的合作、支持、兴趣、批评和强烈反对。”
“批评和强烈反对”——哇!事实上,一些委员会成员对委员会采取的方向非常不满,以至于他们离开了并开始了他们自己的语言定义项目,至少部分原因是抗议 Algol 68。例如,Niklaus Wirth 对 Algol 68 的复杂性感到厌烦,转而设计了 Pascal。并且从大约 1984 年到 2000 年左右编写和支持了相当多的 Pascal 代码,我在这里告诉你,Pascal 与 Algol 68 的差距尽可能的大。在我看来,这正是 Wirth 的观点。
Dennis Ritchie 在 1993 年于马萨诸塞州剑桥举行的第二届 ACM 编程语言历史会议上 发表了演讲,他在演讲中比较了 Bliss、Pascal、Algol 68 和 C。在那次演讲中,他做了一些有趣的观察
- 这四种语言都“基于这种古老的机器模型,即拾取事物,执行操作,然后将它们放在其他地方”,并且“都深受 Algol 60 和 FORTRAN 的影响。”
- “当 Steve Bourne(是的,就是创建 Bourne shell 的人)带着 Algol 68C 编译器来到贝尔实验室时,他让它做了 C 可以做的同样的事情;它具有 Unix 系统调用接口等等。”
- “我认为这种语言确实因其定义的接受程度而受到影响。然而,它实际上非常实用。”
- “在某些方面,Algol 68 是我一直在讨论的语言中最优雅的。我认为在某些方面,它甚至是最有影响力的,尽管作为一种语言本身,它几乎消失了。”
今天在互联网上仍然可以看到更多关于 Algol 68 的观点。其中很多是负面的,但没关系!我怀疑其中很大一部分并非基于实际使用。在 Rosetta Code Wiki 上可以找到刚刚开始使用该语言(以及许多其他语言,其中一些非常晦涩难懂)的程序员。去那里形成你自己的观点!或者跟随我回顾一下我认为 Algol 68 的优点和缺点。
对我来说,Algol 68 的重要性和相关性
Algol 68 作为一种编程语言,提供了一些独特的和有用的想法,这些想法在当时是创新的,并且在一定程度上出现在后来的其他语言中。
修订报告中明确解释的关键设计原则
设计 Algol 68 的委员会受到一套非常明确的原则驱动
- 描述的完整性和清晰度(借助两级语法的使用,这引起了很多负面意见)
- 正交设计;也就是说,语言中定义的基本概念可以在任何可以被认为是“有意义”的用法中使用。例如——每个可以合理预期产生值的表达式实际上都会产生一个值。
- 通过仔细的语法设计(再次是两级语法)实现安全性;在其他语言中被认为与语义概念相关的绝大多数错误都可以在编译时检测到。
- 效率,即程序应该高效运行(在当时的硬件上),而无需付出重大努力来优化生成的代码,此外
- 除了在运行时呈现替代配置的类型(Algol 68 中的
united
类型,类似于 C 中的union
类型)这种独特情况外,没有运行时类型检查 - 类型无关的解析(再次,两级语法在这里起作用)以及确定在有限的步骤中,任何输入序列都可以被评估为程序与否
- 鼓励使用当时的著名循环优化策略的循环结构
- 一个符号集(带有替代方案),可以在当时计算机上可用的各种不同字符集上工作
- 除了在运行时呈现替代配置的类型(Algol 68 中的
我发现,看到 50 年前对非常强的静态类型化的强调以及预期会累积的好处,与当今动态类型语言和弱静态类型语言的世界形成对比,后者帮助催生了整个运行时测试行业,这很有启发意义。(好吧,也许这并不完全公平,但它包含一定的真实成分)。
将语句组合在一起而无需额外分组结构的结构
在用 Algol 60 和 Pascal 编写的程序中,我们看到了很多 begin
和 end
标记;在 C、C++、Java 等等中,我们看到了很多 {
和 }
。例如,计算整数值 iv
的绝对值 av
的简单表达式可以在 Algol 60 或 Pascal 中编写为
if iv < 0 then av := -iv else av := iv
如果我们想设置一个布尔值来声明 iv
是否为负数,那么我们需要开始插入 begin
和 end
if iv < 0 then begin av := -iv; negative := true end else begin av := iv; negative := false end
形式上,Algol 68 对具有特殊含义的标记(如 if 或 then)使用粗体,对事物名称(如 print() 过程)使用斜体。这在当时仍然使用穿孔卡进行编码时是不切实际的,而且即使在今天也会有点奇怪。因此,Algol 68 实现通常提供一些标记特殊符号(称为stropping)的方法,而将所有其他内容保持未标记状态。默认情况下,Algol 68 Genie 使用大写 stropping,因此像 if 这样的符号被编码为 IF,而事物名称只能用小写字母表示。但是,值得注意的是,如果适合手头的目的,完全可以使用名为“if”的变量。无论如何......如果任何读者倾向于复制/粘贴,我在我的代码示例中使用了 Genie 约定。
此外,Algol 68 具有封闭的语法,Bourne shell 和 Bash 继承了这种语法。因此,Algol 68 Genie 中前面的代码行将是
IF iv < 0 THEN av := -iv; negative := TRUE ELSE av := iv; negative := FALSE FI
标记 fi
关闭前面的 if
,以防不明显。现在,也许我是世界上唯一一个编写过如下 Java 代码的人
if (something)
statement;
然后发现自己插入对 println
的调用来调试该代码
if (something)
statement;
System.err.println(stuff); /* not in the then-part of if!!! */
糊里糊涂地忘记将 then-part 包装在 {
… }
中。当然,这并不是世界末日,但是当插入的东西结果不太明显时,好吧,只能说我多年来花费了相当多的时间来调试这类事情。
但这在 Algol 68 中不会发生。好吧,大多数情况下是这样。Algol 68 仍然需要 begin
… end
用于运算符和过程声明。但是 if
… fi
、do
… od
和 case
… esac
(Algol 68 switch 语句)都是封闭的。
我们今天在 Go 中看到了相同的概念;“if”语句看起来像 if … { … };{
和 }
是必需的。正如我已经提到的,Bourne shell 及其后代使用类似的构造。
几乎每个表达式都会产生一个值
看看上面的表达式 iv < 0
;很明显会产生一个值,而且很可能该值是布尔值(true
或 false
)。所以没什么大不了的。
但是赋值语句也会产生一个值,即赋值完成后赋值语句的左侧。
语句序列产生最终语句(或表达式)作为值产生的任何内容。
“if”语句产生 then-part 或 else-part 的值,具体取决于跟在“if”后面的表达式是否产生 true
或 false
。
一个例子:考虑使用 C、Java… 三元运算符来计算我们的绝对值
av = iv < 0 ? -iv : iv;
在 Algol 68 中,我们不需要额外的“三元运算符”,因为“if”语句工作正常
av := IF iv < 0 THEN -iv ELSE iv FI
现在可能是提及 Algol 68 提供了 begin
、end
、if
、then
、else
等符号的“简短”版本的好时机,使用 ( |
和 )
av := ( iv < 0 | -iv | iv )
与之前的表达式具有相同的含义。
当我第一次遇到它时,让我惊讶的一件事是循环不产生表达式。但是循环有一些不同之处,一旦完全理解,就会变得有意义。
Algol 68 中的循环可能看起来像这样
FOR lv FROM 1 BY 1 TO 1000 WHILE 2 * lv * ly < limit DO … OD
这里的变量 ly
是循环变量,由 for
隐式声明为整数。它的作用域是整个 for
… od
,,它的值从一次迭代保留到下一次迭代。我们可以在 while
… do
部分中声明一个常规变量,就像在 if
… then
部分中一样。它的作用域是 while
… od
部分,但它的值不会从一次迭代保留到下一次迭代。因此,例如,如果我们想累加数组元素的总和,我们必须编写
INT sum := 0; FOR ai FROM LWB array TO UPB array DO sum +:= array[ai] OD
其中运算符 lwb
和 upb
分别传递为数组定义的最小和最大索引值,而符号 +:= 与 C 或 Java 中的 += 具有相同的含义。
如果我们想将总和作为值返回,我们将编写
BEGIN INT sum := 0; FOR ai FROM LWB array TO UPB array DO sum +:= array[ai] OD; sum END
当然,为了简洁起见,我们可以用 (
和 )
替换 begin
和 end
。此表达式将是返回数组元素值之和的过程(或运算符)的合理实现。
正交性——相同的表达式几乎可以在任何地方工作
再次查看上面的表达式 iv < 0
。
让我们退后一步,包括 iv
的定义及其值的获取。那么代码可能看起来像
INT iv; read(iv); IF iv < 0 THEN … FI
但是,我们也可以这样写
IF INT iv; read(iv); iv < 0 THEN … FI
在这里我们可以看到正交性在起作用——变量的声明和读取可以发生在 if
和测试变量的逻辑表达式之间,因为传递的值只是最终表达式的值。此外,这与 Algol 68 语义一起工作,以提供一个有趣的差异——在第一种情况下,iv
的作用域是围绕“if”语句的代码;在第二种情况下,作用域仅在 if
和 fi
之间。在我看来,这个选项意味着我们应该减少在远离它们使用的地方声明的变量,而剩下的变量在代码中确实具有“长寿命”。
这在实践中也很重要。例如,考虑使用某种 SQL 接口在数据库中执行多个脚本并返回用于进一步分析的值的代码。通常,在这种情况下,程序员需要做一些工作来设置与数据库的连接,将查询字符串传递给 execute 命令,并检索结果。每个实例都需要声明一些变量来保存连接、查询字符串和结果。当这些变量可以在结果累积代码本地声明时,这是多么好!这也便于通过快速复制粘贴添加新的查询分析步骤。是的,最好将这些代码片段转换为过程调用,尤其是在支持 lambda(匿名过程)的语言中,以避免重复的管理步骤模糊不同的分析步骤。但是拥有非常本地定义的管理变量有助于所需的重构工作。
正交性的另一个重要结果是,我们可以在赋值语句的左侧以及右侧拥有等效于三元运算符的运算符。
让我们假设我们正在处理一个有符号整数的输入流,并且我们想要将正整数累加到收益中,将负整数累加到损失中。那么,以下Algol 68代码可以工作
IF amount < 0 THEN losses +:= amount ELSE gains +:= amount FI
然而,没有必要在此处重复 +:= amount
;我们可以将其移到 if
… fi
之外,如下所示
IF amount < 0 THEN losses ELSE gains FI +:= amount
这之所以有效,是因为 "if" 语句将损失或收益表达式作为测试评估的结果产生,并且该表达式按 amount 递增。当然,我们可以使用简短形式,至少在我看来,这提高了这些简短表达式的可读性
(amount < 0 | losses | gains) +:= amount
来一个真实的例子,展示一下这种面向表达式的东西有多棒?
假设您正在编写一个哈希表工具。您必须实现的两个函数是“获取与给定键关联的值”和“设置与给定键关联的值”。
在面向表达式的语言中,这些可以是一个函数。为什么?因为“get”操作返回找到值的位置,然后“set”操作只是使用“get”操作来设置该位置的值。假设我们创建了一个名为 valueat
的运算符,它接受两个参数——哈希表本身和键值。那么,
ht VALUEAT 42
将返回哈希表 ht 中键 42 的位置,并且
ht VALUEAT 42 := "the meaning of everything"
将字符串“the meaning of everything”放在位置 42。
这减少了支持手头应用程序所需的代码量,减少了必须测试的路径和边缘情况的数量,并且总体上为用户和维护者的生活增添了美妙之处。
在 RosettaCode 上有一个在赋值语句的左侧使用过程来将值存储在表中的简单示例。
匿名过程(lambda 表达式)
现在似乎每个人都想要匿名过程(或“此处”过程,或 lambda 表达式)。Algol 68 开箱即用地提供了这一点,而且它真的非常有用。
举例来说,假设您想要创建一个工具来读取带有分隔字段的文件,并为用户提供与这些文件进行良好交互的途径。想想 awk
在这方面所做的出色工作,基本上是通过抽象掉所有与打开文件、读取行、将行拆分为字段以及沿途提供一些有用的附带变量(如当前行号、此行上的字段数等等)相关的垃圾。
事实证明,这在 Algol 68 中也很容易做到,其中的任务变成了编写一个过程,该过程接受三个参数——第一个是输入文件名,第二个是字段分隔符字符串,第三个是处理每一行的过程。
该过程的声明可能如下所示
PROC each line = # 1 #
(STRING input file name, CHAR separator, PROC (STRING, [] STRING, INT) VOID process) # 2 #
VOID: BEGIN # 3 #
FILE inf; # 4 #
open(inf, input file name, stand in channel); # 5 #
BOOL finished reading := FALSE;
on logical file end (inf, (REF FILE f) bool: finished reading := TRUE); # 6 #
INT linecount := 0; # 7 #
WHILE # 8 #
string line;
get(inf,(line, new line));
not finished reading
DO # 9 #
linecount +:= 1;
FLEX [1:0] STRING fields := split(line, separator);
process(line, fields, linecount)
OD;
close(inf) # 10 #
END # 11 #
以下是上面发生的事情
-
注释 1(上面的 # 1 #)——过程
each line
的声明(请注意,可以随意在名称或数字的中间插入空格) -
each line
的参数——string
文件名,字段分隔符char
字符,以及将要调用的pro
过程来处理每一行,该过程本身接受一个string
(输入行)、一个string
数组(行的字段)和一个int
整数(行号),并返回一个void
值 -
each line
返回一个void
值,并且过程体以begin
开头,允许我们在其定义中使用多个语句 -
声明输入
file
-
将
standard input channel
与file
关联,其名称由input file name
给出并打开它(用于读取) -
Algol 68 处理文件结束条件的方式略有不同;在这里,我们使用 I/O 事件检测过程
on logical file end
来设置标志finished reading
,我们可以在处理文件时检测到该标志 -
创建并初始化行计数(请参阅先前对循环性质的描述)
-
这个
while
循环尝试从输入文件中读取下一行。如果成功,它将处理该行;否则,它将退出 -
处理输入行——递增行计数;使用
split
过程创建与行字段对应的字符串数组;调用提供的process
过程来使用行、其字段和行计数 -
记住
close
文件 -
过程定义的
end
。
我们可以像这样使用它,以便构建一个查找表(与前面部分中顺便提及的假设哈希表工具结合使用)
# remapping definitions in remapping.csv file #
# new-reference|old-reference #
# 093M0770371|093X0012250 #
# 093M0770375|093X0012249 #
# 093M0770370|093X0012133 #
HASTABLE ht := new hashtable;
each delimited line("test.csv", "|", (STRING line, [] STRING fields, INT linecount) VOID: BEGIN
STRING to map := fields[1], from map := fields[2];
ht VALUEAT from map := to map
END);
上面,我们看到了对每个分隔行的调用。特别有趣的是“此处”过程或 lambda 表达式的声明,它将查找值存储到哈希表中。从我的角度来看,这里最大的教训是 lambda 表达式是 Algol 68 正交性的结果;我认为这非常棒。
在我继续探索 Algol 68 时,我计划深入研究的一件事是我可以将这种函数式表达式形式发挥到什么程度。例如,我不明白为什么我不能逐元素构建列表或哈希表元素,并将完成的结构作为循环过程的结果产生,因此上面可能看起来更像
HASHTABLE ht := each delimited line as map entry("test.csv", "|",
(STRING line, [] STRING fields, INT linecount) VOID: BEGIN
STRING to map := fields[1], from map := fields[2];
(from map, to map)
END);
结论
为什么要学习古老、陈旧和被遗忘的语言?嗯,我们都知道最近对 COBOL 的兴趣,但也许这是一个异常值,因为可能没有很多关键任务应用程序是用 SNOBOL、Icon、APL 甚至 Algol 68 编写的。当然,我们应该牢记乔治·桑塔亚纳的指导:“那些不记得过去的人注定要重蹈覆辙。”
对我来说,有几个关键原因让我要提高我在 Algol 68 方面的水平(可能还有一些其他语言,这些语言似乎不是我日常工作绝对必需的)
-
Algol 68 的定义不是为了应对现有编程语言中的一些烦恼;相反,根据修订报告
-
委员会(国际信息处理联合会 ALGOL 2.1 工作组)“表示相信一种通用编程语言为许多国家/地区的许多人服务具有价值。”
-
“Algol 68 的设计目的不是作为 Algol 60 的扩展,而是一种完全基于对计算的基本、基本概念的新见解和一种新的描述技术的新语言。”
-
-
无论是通过复制到其他语言中的积极贡献(Bourne shell 中的
do
…od
;C、Java、… 中的 +=)还是消极反应(Pascal 及其所有后代,Ada),Algol 68 都可以声称对计算产生了深远的影响。 -
虽然 Algol 68 非常“具有时代性”,受到穿孔卡片和行式打印机、小型且多样化的字符集、1960 年代和 1970 年代计算机字符和字长的广泛变化的影响,并且没有明确地包含面向对象或函数式编程,但其非凡的正交性和面向表达式的特性弥补了这些怪异之处以及其他有用的不足之处。
-
也许最实际的原因是在我的桌面上安装并运行了出色的 Algol 68 Genie 解释器,这使我可以追求这个奇怪的小爱好!
也许我应该再次回到桑塔亚纳,听取最后的评论
评论已关闭。