Icon (编程语言)

Icon是一门领域特定高级编程语言,有着“目的(goal)导向执行”特征,和操纵字符串和文本模式的很多设施。它派生自SNOBOL和SL5字符串处理语言[6]。Icon不是面向对象的,但在1996年开发了叫做Idol的面向对象扩展,它最终变成了Unicon

Icon
Panorama Icon logo Mondadori.png
编程范型多范型:面向文本, 结构化, 非确定性
设计者Ralph Griswold英语Ralph Griswold
发行时间1977年,​46年前​(1977
当前版本
  • 9.5.20i (2020年8月2日;稳定版本)[1]
编辑维基数据链接
类型系统动态
许可证公有领域
网站www.cs.arizona.edu/icon
主要实现产品
Icon, Jcon
派生副语言
Unicon
启发语言
SNOBOL[2], SL5[3], ALGOL
影响语言
Unicon, Python[4], Goaldi

历史

在1971年8月,SNOBOL的设计者之一Ralph Griswold英语Ralph Griswold离开了贝尔实验室,成为了亚利桑那大学的教授[7]。他那时将SNOBOL4介入为研究工具[8]

作为最初在1960年代早期开发的语言,SNOBOL的语法带有其他早期编程语言的印记,比如FORTRANCOBOL。特别是,语言是依赖列的,像很多要录入到打孔卡的语言一样,有着列布局是很自然的。此外,控制结构几乎完全基于了分支,而非使用,而块在ALGOL 60中介入之后,已经成为了必备的特征。在他迁移到亚利桑那的时候,SNOBOL4的语法已然过时了[9]

Griswold开始致力于用传统的流程控制结构如if…then,来实现SNOBOL底层的成功和失败概念。这成为了SL5,即“SNOBOL Language 5”的简写,但是结果不令人满意[9]。在1977年,他考虑设计语言的新版本。他放弃了在SL5中介入的非常强力的函数系统,介入更简单的暂停和恢复概念,并为SNOBOL4自然后继者开发了新概念,具有如下的原则[9]

  • SNOBOL4的哲学和语义基础;
  • SL5的语法基础;
  • SL5的特征,排除广义的过程机制。

新语言最初叫做SNOBOL5,但因为除了底层概念外,全都与SNOBOL有着显著的差异,最终想要一个新名字。在这个时候Xerox PARC出版了他们关于图形用户界面的工作,术语“icon”从而进入了计算机词汇中。起初确定为“icon”而最终选择了“Icon”[9]

基本语法

Icon语言派生自ALGOL类的结构化编程语言,因而有着类似CPascal的语法。Icon最类似于Pascal的,是使用了:=语法的赋值,procedure关键字和类似的语法。在另一方面,Icon使用C风格的花括号来结构化执行分组,并且程序开始于运行叫做main的过程。

Icon还在很多方面分享了多数脚本语言(还有SNOBOL及SL5)的特征:变量不需要声明,类型是自动转换的,就说数字和字符串可以自动来回转换。另一个常见于很多而非全部的脚本语言的特征是,缺少行终止字符;在Icon中,不结束于分号的行,若其确有意义则由暗含的分号来终结。

过程是Icon程序的基本建造块。尽管它们使用Pascal名称,但工效上更像C函数并可以返回值;在Icon中没有function关键字。

procedure doSomething(aString)
  write(aString)
end

目的导向执行

Icon的关键概念之一就是其控制结构基于表达式的“成功”或“失败”,而非大多数其他编程语言中的布尔逻辑。这个特征直接派生自SNOBOL,在其中表达式求值、模式匹配和模式匹配连带替换,都可以跟随着成功或失败子句,用来指定在这个条件下要分支到一个语句标签。例如,下列代码打印“Hello, World!”五次[10]

* 打印Hello, World!五次的SNOBOL程序 
      I = 1
LOOP  OUTPUT = "Hello, World!"
      I = I + 1
      LE(I, 5) : S(LOOP)
END

要进行循环,在索引变量I之上调用内建的函数LE(小于等于),并且如果S(成功),意味着I小于等于5,它分支到命名标签LOOP并继续下去[10]

Icon保留了基于成功或失败的控制流程的基本概念,但进一步发展了语言。一个变更是将加标签的GOTO式的分支,替代为面向块的结构,符合在1960年代后期席卷计算机工业的结构化编程风格[9]。另一个变更是允许失败沿着调用链向上传递,使得整个块作为一个整体的成功或失败。这是Icon语言的关键概念。而在传统语言中,必须包括基于布尔逻辑的测试成功或失败的代码,并接着基于产出结果进行分支,这种测试和分支是固有于Icon代码的,而不需要明确的写出[11]。考虑如下复制标准输入标准输出的简单代码:

while a := read() then write(a)

它的含义是:“只要读取不返回失败,调用写出,否则停止”[12]。在Icon中,read()函数返回一行文本或&fail&fail不是简单的Java中的特殊返回值EOF(文件结束)的类似者,因为它被语言依据上下文明确理解为意味着“停止处理”或“按失败状况处理”。这里即使read()导致一个错误它都会工作,比如说如果文件不存在。在这种情况下,语句a := read()会失败,而写操作简单的不调用。

成功和失败将沿着调用链向上传递,意味着可以将函数嵌入其他函数内,在嵌套函数英语nested function失败时,它们整体停止。例如,上面的代码可以精简为[13]

while write(read())

read()命令失败的时候,比如在文件结束之处,失败将沿着调用链上传,而write()也会失败。while作为一个控制结构,在失败时停止。Icon称谓这个概念为“目的导向执行”,指称这种只要某个目的达到执行就继续的方式。在上面的例子中目的是读整个文件;读命令在有信息读到的时候成功,而在没有的时候失败。目的因此直接编码于语言中,不用再去检查返回码或类似的构造。

Icon使用目的导向机制用于进行传统的布尔测试,尽管有着微妙的差异。一个简单的比较如if a < b then write("a is smaller than b"),这里的if子句,不像在多数语言中那样意味着:“如果右侧运算求值为真”;转而它的意味更像是:“如果右侧运算成功”。在这种情况下,如果这个比较为真,<算子成功。如果if子句的这个表达式成功,则调用then子句,如果它失败了,则调用else子句或下一行。结果同于在其他语言中见到的传统if…then,如果a小于bif进行then子句。微妙之处是相同的比较表达式可以放置在任何地方,例如:

write(a < b)

另一个不同是<算子如果成功,返回它的第二个实际参数,在这个例子中,如果b大于a,则导致它的值被写出,否则什么都不写。因为并非测试本身,而是一个算符返回一个值,它们可以串联在一起,允许像if a < b < c这样的事情[13] ,在多数语言中平常类型的比较下,必须写为两个不等式的结合,比如if (a < b) && (b < c)

将成功和失败的概念与异常的概念相对比是很重要的;异常是不寻常的状况,不是预期的结果。在Icon中失败是预期的结果;到达文件的结束处是预期的状况而不是异常。Icon没有传统意义上的异常处理,尽管失败经常被用于类似异常的状况下。例如,如果要读取的文件的不存在,read失败而不指示出特殊状况[12]。在传统语言中,没有指示这些“其他状况”的自然方式,典型的异常处理是“抛出”一个值,下面是用Java处理缺失文件的例子:

try {
    while ((a = read()) != EOF) {
        write(a);
    }
} catch (Exception e) {
    // 某个事情出错了,使用这个catch来退出循环
}

这种情况需要两个比较:一个用于文件结束(EOF)而另一个用于所有其他错误。因为Java不允许异常作为逻辑元素来比较,就像Icon中那样,转而必须使用冗长的try/catch语法。try块即使没有异常抛出,也强加了性能上的惩罚,Icon避免了这种分摊成本英语Distributed cost

目的导向执行的一个关键方面,是程序可能必须在一个过程失败时倒转到以前的状态,这个任务叫做回溯。例如,考虑设置一个变量为一个开始位置,并接着进行可以改变这个值的操作,这是在字符串扫描中常见情况,这里前进游标通过它所扫描的字符串。如果这个过程失败了,任何对这个变量的后续读取都返回最初的状态,而非被内部操纵后的状态是很重要的。对于这种任务,Icon有一个“可逆赋值”算子<-,和“可逆交换”算子<->。例如,考虑如下尝试在一个更大字符串内找到一个模式字符串的代码:

{
  (i := 10) &
  (j := (i < find(pattern, inString)))
}

这个代码开始于移动i10,这是查找的开始位置。但是,如果find失败,这个块将作为整体失败,作为一个不想要的副作用,它导致i的值留下为10。故而应将i := 10替代为i <- 10,指示i在这个块失败时应当被重置为它以前的值。这提供了执行中的原子性英语Atomic commit的类似者。

生成器

在Icon中表达式经常返回一个单一的值,例如5 > x,将求值并且如果x的值小于5则成功并返回x,否则失败。但是,Icon还包括了过程不立即返回成功或失败,转而每次调用它们之时返回一个新值的概念。这些过程叫做生成器,并且是Icon语言的关键部分。在Icon的用语中,一个表达式或函数的求值产生一个“结果序列”。结果序列包含这个表达式或函数生成的所有可能的值。在结果序列被耗尽的时候,这个表达式或函数失败。

Icon允许任何过程返回一个单一值或多个值,使用failreturnsuspend关键字来控制。缺乏任何这种关键字的过程返回&fail,它在执行进行到一个过程的end处的时候发生。例如:

procedure f(x)
  if x > 0 then {
    return 1
  }
end

调用f(5)将返回1,而调用f(-1)将返回&fail。这将导致不明显的行为,比如write(f(-1))将什么都输出,因为f失败而暂停了write的操作[14]

将一个过程转换成一个生成器,要使用suspend关键字,它意味着“返回这个值,并且在再次调用时,从这一点开始执行”。例如[12]

procedure ItoJ(i, j)
  while i <= j do {
    suspend i
    i +:= 1
  }
  fail
end

创建一个生成器,它返回一系列的数,开始于i并结束于j,接着在它们之后返回&fail[a]suspend i停止执行,并返回i的值,而不重置任何状态。当对相同函数做出另一次调用的时候,执行在这一点上拾起以前的值。在这种情况下,导致它进行i +:= 1,循环回到while的开始处,并接着返回下一个值并再次暂停。这将持续直到i <= j失败,在这一点上它退出这个块并调用fail。这允许轻易的构造迭代器[12]

另一种类型的生成器建造器是“交替英语Alternation (formal language theory)子”(alternator),它的感观和运算就像布尔算子or,例如:

if y < (x | 5) then write("y=", y)

这看起来是在说“如果y小于x或者5那么...”,实际上它是生成器的一种简写形式,它返回值直到脱离于这个列表的结束处。这个列表的值被注入到运算之中,在这里是<。所以这个例子,系统首先测试y < x,如果x实际上大于y,它返回x的值,这个测试通过,而y的值在then子句中写出。然而,如果x不大于y,它失败了,交替子继续,进行y < 5。如果这个测试通过,写出y。如果y不小于x或者5,交替子用完了,测试失败,if子句失败,而不进行write。因此,y的值如果小于x5,则它将出现在控制台上,从而履行了布尔or的作用。函数不会被调用,除非求值它们的参数成功,所以这个例子可以简写为:

write("y=", (x | 5) > y)

在内部,交替子不是简单的一个or,它还可以用来构造值的任意列表。这可以用来在任意的一组值上迭代,比如:

every i := (1|3|4|5|10|11|23) do write(i)

every类似于while,循环经过一个生成器的返回的所有项目,在失败时退出[14]

因为整数列表在很多编程场景都是很常见的,Icon还包括了to关键字来构造“事实上的”整数生成器:

every k := i to j do write(k)

在这种情况下,从ij的值,将注入到write并写出多行输出[14]。它可以简写为:

every write(i to j)

Icon不是强类型的,所以交替子列表可以包含不同类型的项目:

every i := (1 | "hello" | x < 5)  do write(i)

这将依赖于x的值,写出1"hello"或可能的5

同样的“合取算符”&,以类似于布尔算符and的方式来使用[15]

every x := ItoJ(0,10) & x % 2 == 0 do write(x)

这个代码调用ItoJ并返回一个初始值0,它被赋值给x。接着进行合取的右手端,并且因为x % 2不等于0,它写出这个值。接着再次调用ItoJ生成器,它赋值1x,这使得右手端失败而不打印任何东西。最终结果是从010的所有偶数的一个列表[15]

生成器的概念对于字符串操作是很强大的。在Icon中,find函数是个生成器。下面的例子代码,在一个字符串中找出"the"的所有出现位置:

s := "All the world's a stage. And all the men and women merely players"
every write(find("the", s))

find在每次被every恢复的时候,将返回"the"的下一个实例的索引,最终达到字符串结束处并失败。

当然人们有时会想要找到在输入中某点之后的一个字符串,例如,扫描包含多列数据的一个文本文件。目的导向执行也能起效:

write(5 < find("the", s))

只返回"the"出现在位置5之后的那些位置;否则比较会失败。成功的比较返回右手侧的结果,所以把find放置到这个比较的右手侧是重要的。

搜集

Icon包括了一些搜集类型,包括列表,它还可以用作堆栈队列表格(在其他语言中也叫做映射或字典),和集合英语Set (abstract data type)等。Icon称它们为“结构”。搜集是固有的生成器,并可以使用“叹号语法”来轻易调用。例如:

lines := []                    # 建立一个空列表
while line := read() do {      # 循环从标准输入读取行
  push(lines, line)            # 使用类堆栈语法来将行压入列表
}
while line := pop(lines) do {  # 循环于行可以从列表弹出之时
  write(line)                  # 将行写出
}

使用如前面例子中见到的失败传播,可以组合测试和循环:

lines := []                    # 建立空列表
while push(lines, read())      # 压入直到为空
while write(pop(lines))        # 写直到为空

由于列表搜集是个生成器,可以使使用叹号语法进一步简化:

lines := []
every push(lines, !&input)
every write(!lines)

在这种情况下,在write内的叹号,导致Icon从数组一个接一个的返回一行文本,并且在结束处失败。&input是基于生成器的read的类似者,它从标准输入读取一行,所以!&input继续读取行直到文件结束。

因为Icon是无类型的,列表可以包含任何不同类型的值:

aCat := ["muffins", "tabby", 2002, 8]

在列表内的项目可以包括其他结构。为了建造更大的列表,Icon包括了list生成器;i := list(10, "word")生成包含"wold"10个复本的一个列表。

就像其他语言中的数组,Icon允许项目按位置来查找,比如weight := aCat[4]。就像数组分片英语Array slicing那样,索引是在元素之间的,可以通过指定范围来获得列表的分片,比如aCat[2:4]产生列表["tabby",2002]

表格本质上是具有任意索引键而非仅为整数的列表:

symbols := table(0)
symbols["there"] := 1
symbols["here"] := 2

这个代码创建使用的0作为任何未知键的缺省值的一个table。接着向它增加了两个项目,具有键"there""here",和分别的值12

集合也类似于列表,但是只包含任何给定值的一个单一成员。Icon包括了++来产生两个集合的并集,**用于交集,和--用于差集。Icon包括一些预定义的Cset,即包含各种字符的集合。在Icon中有四个标准Cset&ucase&lcase&letters&digits。可以通过用单引号包围字符串来建造Cset,例如vowel := 'aeiou'.

字符串

在Icon中,字符串是字符的列表。作为一个列表,它们是生成器,并可以使用“叹号语法”来迭代:

write(!"Hello, world!")

这将在独立行上打印出字符串的每个字符。

子字符串可以使用在方括号内的一个范围规定从字符串中提取出来。范围规定可以返回到一个单一字符的一个点,或字符串的一个分片(slice)。字符串可以从左或从右索引。在一个字符串内的位置被定义为在字符之间:1A2B3C4,也可以从右规定:−3A−2B−1C0。例如:

"Wikipedia"[1]     == "W"
"Wikipedia"[3]     == "k"
"Wikipedia"[0]     == "a"
"Wikipedia"[1:3]   == "Wi"
"Wikipedia"[-2:0]  == "ia"
"Wikipedia"[2+:3]  == "iki"

这里最后例子采用了x1[i1+:i2] : x2表达式,产生x1i1i1 + i2之间的子字符串。

子字符串规定可以用作字符串内的左值。这可以被用来把字符串插入到另一个字符串,或删除字符串的某部分。例如:

s := "abc"
s[2] := "123"
# s现在的值是"a123c"
s := "abcdefg"
s[3:5] := "ABCD"
# s现在的值是"abABCDefg"
s := "abcdefg"
s[3:5] := ""
# s现在的值是"abefg"

Icon的下标索引是在元素之间的。给定字符串s := "ABCDEFG",索引是1A2B3C4D5E6F7G8。分片s[3:5]是在索引35之间的字符串,它是字符串"CD"

字符串扫描

对处理字符串的进一步简化是“扫描”系统,通过?来发起,它在一个字符串上调用函数:

s ? write(find("the"))

Icon称呼?的左手端为“主语”,并将它传递到字符串函数中。所调用的find接受两个参数,查找的文本作为参数一,而要在其中查找的字符串是参数二。使用?,第二个参数是隐含的,而不由编程者来指定。在多个函数被依次调用在一个单一字符串上的常见情况下,这种风格可以显著的所见结果代码的长度并增加清晰性。

?不是简单的一种语法糖,它还为任何随后的字符串操作,创建一个“字符串扫描环境”。这基于了两个内部变量,&subject&pos,这里的&subject是要扫描的字符串,而&pos是在这个主语字符串内的“游标”或当前位置。例如:

s := "this is a string"
s ? write("subject=[",&subject,"] pos=[",&pos,"]")

将产生:

subject=[this is a string] pos=[1]

内建和用户定义的函数,可以被用于在要扫描的字符串上移动。所有内建函数缺省采用&subject&pos,来允许用上扫描语法。比如函数tab (i) : s,它设置扫描位置:产生&subject[&pos:i],并将i赋值到&pos。下列例子代码,写出在一个字符串内,所有空白界定出的word

s := "this is a string"
s ? {                           # 建立字符串扫描环境
  while not pos(0) do {         # 测试字串结束
    tab(many(' '))              # 跃过任何空白
    word := tab(upto(' ') | 0)  # 下一个word是直到下一个空白或行结束
    write(word)                 # 写这个word
  }
}

这个例子介入了一些新函数。pos返回&pos的当前值。为何需要这个函数,而不简单的直接使用&pos的值,不是显而易见的;原因是&pos是一个变量,而不能呈现值&fail,而过程pos能。因此pos提供对&pos的轻量级包装,它允许轻易使用Icon的目的导向控制流,而不用针对&pos提供手写的布尔测试。在这种情况下,测试是“&pos是零”,在Icon的字符串位置的特异编码中,零是行结束。如果它不是零,pos返回&fail,它通过not反转而使得循环继续。

many从当前&pos开始,找到提供的Cset参数的一个或多个例子。在这种情况下,它查找空格字符,所以这个函数的结果是在&pos之后的第一个非空格字符的位置。tab移动&pos到那个位置,这种情况下再次具有潜在的&fail,例如many在字符结束处脱离。upto本质上是many的反函数;它返回紧前于提供的Cset的例子的位置,接着由另一个tab来设置&pos。这里的交替用来在行结束处也停止。

这个例子通过使用更合适的“字分隔”Cset,可以包括句号、逗号和其他标点,还有其他空白字符如tab和不换行空格,能够变得更加健壮。这个Cset可以接着用于manyupto

一个更复杂的例子演示了在这个语言内生成器和字符串扫描的集成:

procedure main()
  s := "Mon Dec 8"
  s ? write(Mdate() | "not a valid date")
end
# 定义一个匹配函数
# 它返回匹配day month dayofmonth的一个字符串
procedure Mdate()
# 定义一些初始值
static dates
static days
initial {
  days := ["Mon","Tue","Wed","Thr","Fri","Sat","Sun"]
  dates := ["Jan","Feb","Mar","Apr","May","Jun",
            "Jul","Aug","Sep","Oct","Nov","Dec"]
}
every suspend (retval <- tab(match(!days)) ||  # 匹配一个day
                =" " ||                        # 跟随着一个空白
                tab(match(!dates)) ||          # 跟随着一个month
                =" " ||                        # 跟随着一个空白
                matchdigits(2)) &              # 跟随着最多2位数字
              (=" " | pos(0)) &                # 要么是空白要么是字符串结束
              retval                           # 最终返回这个字符串
end
# 返回最多n位数字的一个字符串的匹配函数
procedure matchdigits(n)
  suspend (v := tab(many(&digits)) & *v <= n) & v
end

表达式*x计算x的大小。这里介入了内建函数match (s1,s2,i1,i2) : i3,它匹配初始字符串:如果s1 == s2[i1+:*s1],产生i1 + *s1,否则失败;它设定有缺省值:s2&subject,如果s2缺省了,i1&pos,否则为1i20

参见

  • 协程

注解

  1. ^ fail在这种情况下是不要求的,因为它紧前于end,增加它是为了清晰性。

引用

  1. ^ https://github.com/gtownsend/icon/releases; 检索日期: 2021年3月19日.
  2. ^ Griswold, Ralph E.; Poage, J.F.; Polonsky, Ivan P. The SNOBOL 4 Programming Language 2nd. Englewood Cliffs NJ: Prentice-Hall. 1971. ISBN 0-13-815373-6. 
  3. ^ Ralph E. Griswold, David R. Hanson, "An Overview of SL5", SIGPLAN Notices 12:4:40-50 (April 1977)
  4. ^ Schemenauer, Neil; Peters, Tim; Hetland, Magnus. PEP 255 -- Simple Generators. 2001-12-21 [2008-09-05]. (原始内容存档于2020-06-05). 
  5. ^ v9.5.22e. [2022-11-09]. (原始内容存档于2022-11-11). 
  6. ^ Griswold, Ralph E.; Griswold, Madge T. History of the Icon programming language. Bergin, Thomas J.; Gibson, Richard G. (编). History of Programming Languages II. New York NY: ACM Press. 1996. 
  7. ^ Griswold 1981,第609页.
  8. ^ Griswold 1981,第629页.
  9. ^ 9.0 9.1 9.2 9.3 9.4 Griswold & Griswold 1993,第53页.
  10. ^ 10.0 10.1 Lane, Rupert. SNOBOL - Introduction. Try MTS. 26 July 2015 [2022-02-03]. (原始内容存档于2022-05-09). 
  11. ^ Tratt 2010,第73页.
  12. ^ 12.0 12.1 12.2 12.3 Tratt 2010,第74页.
  13. ^ 13.0 13.1 Griswold 1996,第2.1页.
  14. ^ 14.0 14.1 14.2 Tratt 2010,第75页.
  15. ^ 15.0 15.1 Tratt 2010,第76页.

参考书目

外部链接