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

Icon
编程范型多范型:面向文本, 结构化
設計者Ralph Griswold英语Ralph Griswold
发行时间1977年,​47年前​(1977
当前版本
  • v9.5.23a(2023年8月19日;滚动更新)[1]
  • 951(2013年6月5日;穩定版本)[2]
編輯維基數據鏈接
型態系統动态
許可證公有领域
網站www.cs.arizona.edu/icon
主要實作產品
Icon, Jcon
衍生副語言
Unicon
啟發語言
SNOBOL[3], SL5[4], ALGOL
影響語言
Unicon, Python[5], 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 main() 
    write("Hello, world!")
end

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()的操作[10]

目标导向执行

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

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

要进行循环,在索引变量I之上调用内建的函数LE()(小于等于),并且S(LOOP)测试它是否成功,即在I小于等于5之时,分支到命名标签LOOP而继续下去[11]

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

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

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

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

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这样的东西[14],在多数语言中平常类型的比较下,必须写为两个不等式的结合,比如if (a < b) && (b < c)

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

例如,考虑如下尝试在一个更大字符串内找到一个模式字符串的代码块

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

这个代码块开始于移动i10,这是查找的开始位置。但是,如果find()失败,这个块将作为整体失败,作为一个不想要的副作用,它导致i的值留下为10。故而应将i := 10替代为i <- 10

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

在这个块失败时,i会被重置为它以前的值。这提供了在执行中的类似于原子性英语Atomic commit的东西。

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

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

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

生成器

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

因为整数列表在很多编程场景都是很常见的,Icon包括了to算子来构造整数生成器:

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

遍历生成器所生成的所有结果,需要使用表达式every expr1 do expr2,这里的do子句是可选的,它针对expr1生成的每个结果求值expr2,在expr1不再产生结果时失败。

算子to的优先级高于赋值。在这种情况下,从ij的值,将注入到write()并写出多行输出[10]。它可以简写为:

every write(i to j)

还可以结合上“合取算子”&,它的使用方式类似于布尔算子and,其优先级在中缀算子中最低[15]

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

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

最常见类型的生成器建造器是|即“交替英语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)

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

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

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

使用关键字suspend可以将一个过程转换成一个生成器。表达式suspend expr1 do expr2,这里的do子句是可选的;它暂停当前过程,将expr1生成的每个结果作为这个过程产生结果返回;在再次调用这个过程之时于此处恢复执行,此时求值expr2于恢复expr1之前。例如:自己定义一个ItoJ生成器[13]

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。这允许轻易的构造迭代器[13]

生成器的概念对于字符串操作是很强大的。在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称它们为“结构”。

因为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'.

搜集是固有的生成器。例如:

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

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

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

表达式!x1 : x2, x3, ..., xn生成多个值:

  • 如果x1是一个文件!x1产生x1余下的诸行。
  • 如果x1是一个字符串!x1生成x1的一个字符的诸子串,并且如果x1是变量则产生诸变量。
  • 如果x1是一个列表、表格或记录,!x1生成具有x1诸元素的诸变量。对于列表和记录,生成的次序是从开始至结束,而对于表格是不可预测的。
  • 如果x1是一个集合,!x1以不可预测的次序生成x1的诸成员。

列表搜集可以使使用叹号语法进一步简化:

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

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

在Icon中,字符串字符列表,可以使用“叹号语法”来迭代:

every write(!"Hello, world!")

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

字符串分片

字符串字符列表,可以使用在方括号内的一个范围规定从字符串中提取出子字符串。子字符串范围规定,可以返回到一个单一字符的一个点,或字符串的一个分片英语array slicing(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是在这个主语字符串内的当前位置或“游标”。

表达式x ? expr : x,保存当前的主语和位置,并接着分别设置它们为x1,接着求值expr;它的产出就是expr的产出,在从expr退出时它将主语和位置复原为保存的值。例如:

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

将产生:

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

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

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

将产生:

this
is
a
string

这个例子介入了一些新函数。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可以接着用于many()upto()

结合了生成器与字符串扫描的一个更复杂的例子:

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

将产生:

Mon Dec 8

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

参见

注解

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

引用

  1. ^ https://github.com/gtownsend/icon/releases/tag/v9.5.23a.
  2. ^ Release 951. 2013年6月5日 [2023年9月19日]. 
  3. ^ 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. 
  4. ^ Ralph E. Griswold, David R. Hanson, "An Overview of SL5", SIGPLAN Notices 12:4:40-50 (April 1977)
  5. ^ Schemenauer, Neil; Peters, Tim; Hetland, Magnus. PEP 255 -- Simple Generators. 2001-12-21 [2008-09-05]. (原始内容存档于2020-06-05). 
  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 Tratt 2010,第75頁.
  11. ^ 11.0 11.1 Lane, Rupert. SNOBOL - Introduction. Try MTS. 26 July 2015 [2022-02-03]. (原始内容存档于2022-05-09). 
  12. ^ Tratt 2010,第73頁.
  13. ^ 13.0 13.1 13.2 13.3 Tratt 2010,第74頁.
  14. ^ 14.0 14.1 Griswold 1996,第2.1頁.
  15. ^ 15.0 15.1 Tratt 2010,第76頁.

参考书目

外部链接