Icon语言
来自维基百科,自由的百科全书
Icon是一门领域特定的高级编程语言,有着“目标(goal)导向执行”特征,和操纵字符串和文本模式的很多设施。它派生自SNOBOL和SL5字符串处理语言[6]。Icon不是面向对象的,但在1996年开发了叫做Idol的面向对象扩展,它最终变成了Unicon。
历史
在1971年8月,SNOBOL的设计者之一Ralph Griswold离开了贝尔实验室,成为了亚利桑那大学的教授[7]。他那时将SNOBOL4介入为研究工具[8]。
作为最初在1960年代早期开发的语言,SNOBOL的语法带有其他早期编程语言的印记,比如FORTRAN和COBOL。特别是,语言是依赖列的,像很多要录入到打孔卡的语言一样,有着列布局是很自然的。此外,控制结构几乎完全基于了分支,而非使用块,而块在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类的结构化编程语言,因而有着类似C或Pascal的语法。Icon最类似于Pascal的地方,是使用了:=
语法的赋值,procedure
关键字和类似的语法。在另一方面,Icon使用C风格的花括号来结构化执行分组,并且程序开始于运行叫做main
的过程。
Icon还在很多方面分享了多数脚本语言(还有SNOBOL及SL5)的特征:变量不需要声明,类型是自动转换的,就说数字和字符串可以自动来回转换。另一个常见于很多而非全部的脚本语言的特征,是脚本中缺少行终止字符;在Icon中,对于不结束于分号的行,由Icon编译器自动的插入分号来终结,而这种插入的前提是它不会导致跨行的表达式遭到错误的分隔。
过程是Icon程序的基本建造块。尽管它们使用Pascal名称,但工效上更像C函数并可以返回值;在Icon中没有function
关键字。
Icon允许任何过程,执行return
或suspend
语句,来分别返回一个单一值或依次返回多个值中的一个值。过程已经执行到它的end
处,或者执行了fail
语句,它会返回&fail
。例如:
这里的fail
在不是必需的,因为它紧前于end
,增加它是为了清晰性。调用fn(5)
将返回1
,而调用fn(-1)
将返回&fail
。这将导致不明显的行为,比如write(fn(-1))
由于fn(-1)
失败而导致write()
也跟着中止了;还有x := fn(-1)
对x
赋值也不会发生[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]。考虑如下复制标准输入到标准输出的简单代码:
它的含义是:“只要读取不返回失败,调用写出,否则停止”[13]。在Icon中,read()
函数返回一行文本或&fail
。&fail
不同于C语言中的特殊返回值EOF
(文件结束),它被语言依据上下文明确理解为意味着“停止处理”或“按失败状况处理”。这里即使read()
导致一个错误它都会工作,比如说如果文件不存在。在这种情况下,语句a := read()
会失败,而write()
简单的不被调用。
成功和失败将沿着调用链向上传递,意味着可以将函数调用嵌入其他函数调用内,在嵌套的函数调用失败时,它们整体停止。例如,上面的代码可以精简为[14]:
在read()
命令失败的时候,比如在文件结束之处,失败将沿着调用链上传,而write()
也会失败。while
作为一个控制结构,在失败时停止。Icon称谓这个概念为“目标导向执行”,指称这种只要某个目标达到执行就继续的方式。在上面的例子中目标是读整个文件;读命令在有信息读到的时候成功,而在没有的时候失败。目标因此直接编码于语言中,不用再去检查返回码或类似的构造。
Icon使用目标导向机制用于进行传统的布尔测试,尽管有着微妙的差异。一个简单的比较如if a < b then write("a is smaller than b")
,这里的if
子句,不像在多数语言中那样意味着:“如果测试运算求值为真”;转而它的意味更像是:“如果测试运算成功”。在这个例子情况下,如果这个比较为真,则<
运算成功。如果if
子句测试这个表达式成功,则调用then
子句,如果它失败了,则调用else
子句或下一行。结果同于在其他语言中见到的传统if ~ then ~
,它表示如果a
小于b
为真,则执行then
关联的语句。微妙之处是相同的比较表达式可以放置在任何地方,例如:
另一个不同是<
算子如果成功,返回它的第二个运算元,在这个例子中,如果b
大于a
,则导致b
的值被写出,否则什么都不写。因为并非测试本身,而是一个算子返回一个值,它们可以串联在一起,允许如下这样的链式测试[14]:
在多数语言中的平常类型的比较之下,同样的语义必须写为两个不等式合取,比如在C语言中表达为a < b && b < c
。在Icon中,按数值序比较和按词典序比较,采用了不同的中缀算子,比如采用=
进行数值比较,采用==
进行字符串比较,以此类推。
需要注意如果测试一个变量比如c
,意图确定它是否已初始化,它在未初始化时返回一个值&null
,所以对它的测试总是成功的,故而需要测试c === &null
或c ~=== &null
[10]。
目标导向执行的一个关键方面,是程序可能必须在一个过程失败时倒转到以前的状态,这种任务叫做回溯。例如,考虑设置一个变量为一个开始位置,并接着进行可以改变这个值的操作,这是在字符串扫描中常见情况,这里前进游标通过它所扫描的字符串。如果这个过程失败了,任何对这个变量的后续读取都返回最初的状态,而非被内部操纵后的状态是很重要的。对于这种任务,Icon有一个可逆(reversible)赋值算子<-
,和可逆交换算子<->
。
例如下列表达式:
其中的find()
在指定字符串中查找符合特定模式的子字符串并依次返回其起始位置。这个表达式由其优先级比赋值:=
更低的&
分隔为两部分,在第一部分中设置了一个阈值i
为10
。在第二个部分中,如果find()
能成功返回了一个值并且这个值大于阈值,则i < find(pattern, inString)
成功,接着将这个值赋值到j
;否则失败,对j
的赋值和整个表达式都会一起失败。作为在失败情况下不想要的副作用,这个表达式的第一部分会导致i
的值遗留为10
,故而应将i := 10
替代为i <- 10
:
在这个表达式失败之时,i
会被重置为它以前的值。这提供了在执行中的类似于原子性的机制。
将成功和失败的概念与例外的概念相对比是很重要的:异常是不寻常的状况,不是预期的结果;而例外是预期的结果,比如读取文件时到达文件的结束处,是预期的例外状况而不是异常。Icon没有传统意义上的例外处理,尽管失败经常被用于类似例外的状况下。例如,如果要读取的文件的不存在,read()
失败而不指示出特殊状况[13]。在传统语言中,没有指示这些“其他状况”的自然方式,典型的例外处理是throw
“抛出”一个值。例如Java使用try
/catch
语法来处理例外:
try {
// 读取文件等等。
} catch (EOFException e) {
// 遇到文件结束例外
} catch (IOException e) {
// 发生IO异常
}
生成器
在Icon中表达式经常返回一个单一的值,例如5 > x
,将求值并且如果x
的值小于5
则成功并返回x
,否则失败。但是,Icon还包括了过程不立即返回成功或失败,转而每次调用它们之时返回一个新值的概念。这些过程叫做生成器,并且是Icon语言的关键部分。在Icon的用语中,一个表达式或函数的求值产生一个“结果序列”。结果序列包含这个表达式或函数生成的所有可能的值。在结果序列被耗尽的时候,这个表达式或函数失败。
因为整数列表在很多编程场景都是很常见的,Icon包括了to
中缀表达式来构造整数生成器:
遍历生成器所生成的所有结果,可以使用表达式every expr1 do expr2
,这里的do
子句是可选的,它针对expr1
生成的每个结果求值expr2
,在expr1
不再产生结果时失败。
中缀表达式to
的优先级高于赋值算子。在这种情况下,从i
到j
的值,将注入到write()
并写出多行输出[10]。它可以简写为:
在Icon中,经常将合取算子&
用于控制流程,它的使用方式类似于C语言和Bourne Shell中短路求值的逻辑与算子&&
,需要注意不同于C语言的情况,&
的优先级低于赋值算子:=
[15]:
这段代码调用整数生成器并得到其初始值0
,它被赋值给x
。接着进行合取的右手端,因为x % 2
确实等于0
而成功,进而合取x := 0 to 10 & x % 2 == 0
成功,随即执行write()
写出x
的值。接着再次调用这个生成器,它赋值1
到x
,这使得合取的右手端失败进而合取失败,故而不写出任何东西。最终结果是写出从0
到10
的一个列表中的所有偶数[15]。
生成器的概念对于字符串操作是很强大的。在Icon中,find()
函数是个生成器。下面的例子代码,在一个字符串中找出"the"
的所有出现位置:
find()
在每次被every
恢复的时候,将返回"the"
的下一个实例的索引,最终达到字符串结束处并失败。
在想要在一个字符串的某点之后的进行查找之时,目标导向执行也能起效:
如果找出的"the"
出现在位置5
之后,则比较成功并返回右手侧的结果,否则比较失败进行下一次查找。这里把find()
放置到这个比较的右手侧是重要的。
最常见类型的生成器建造器是|
即交替(alternation)表达式,求值交替者的严格语法描述为:expr1 | expr2 : x1, x2, ...
,expr1 | expr2
首先生成expr1
的结果,然后生成expr2
的结果,最终依次产生多个值x1, x2, ...
。它可以用来构造值的任意列表。例如可以在任意的一组值上进行迭代:
Icon不是强类型的,所以交替表达式形成的列表可以包含不同类型的项目:
这将写出1
、"hello"
和在x < 5
的情况下出现的5
。
交替表达式的优先级低于比较算子,高于to
表达式和赋值算子:=
。它在条件语句中担当C语言和Bourne Shell中短路求值的逻辑或算子||
功能,例如:
这段代码中的|
符号起到了逻辑或的作用:如果y
小于x
或者5
,那么写出y
的值。实际上x | 5
是生成器的一种简写形式,它依次返回这个列表的值直到脱离于这个列表结束处,而这个列表的值被注入到这里的<
运算之中。首先测试y < x
,如果x
实际上大于y
,则这次测试成功并且if
子句成功;否则这次测试失败而交替运算继续,从而再次测试y < 5
。如果直到交替运算完成而所有测试都未通过,则if
子句失败。在if
子句成功之后,才会执行then
子句write()
写出y
的值。
函数只有在求值它们的参数成功之后才会被调用,所以这个例子可以简写为:
要将一个过程转换成一个生成器,可以使用表达式suspend expr1 do expr2
,这里的do
子句是可选的;它暂停当前过程,将expr1
生成的每个结果作为这个过程所产生结果返回。在再次调用这个过程之时于这个暂停之处恢复执行,此时先求值expr2
再恢复expr1
;如果expr1
是生成器并产生了新结果,则再次暂停并返回它的结果;如果expr1
不是生成器或者是生成器但不再产生新结果,则继续执行后面的语句。
例如:自己定义一个ItoJ
生成器[13]:
它建立一个生成器,它返回一系列的数,开始于i
并结束于j
,接着在它们之后返回&fail
。suspend i
停止执行,并返回i
的值,而不重置这个函数的任何状态。当对相同函数做出另一次调用的时候,在这一点上于以前的状态下恢复执行。它接着进行i +:= 1
,然后循环回到while
的开始处,接着返回下一个值并再次暂停。这个循环将持续直到i <= j
失败,这时调用fail
退出。这种机制允许轻易的构造迭代器[13]。
将例子稍作修改:
将产生:
1
1
2
4
数据结构
Icon将值的搜集称为“结构”,包括:记录、列表、集合和表格。
记录是固定大小的并且它们的值通过字段名来提及。记录像过程那样声明并且对整个程序而言是全局的,一个记录的实例可以通过记录构造子来创建。例如:
记录的字段通过名字.字段
形式的表达式来提及,例如clerk.salary
。
列表在Icon有两个角色。它是可用下标定位的一维数组(向量),也是可用专属访问函数操纵从而增长或缩短的堆栈和队列。因为Icon是无类型的,列表可以包含任何不同类型的值:
就像其他语言中的数组,Icon允许项目按编号从1
开始的位置来查找,例如salary := clerk[4]
。在Icon中,可以通过指定范围来获得列表的分节(section),就像阵列分片那样,索引设立在元素之间,比如clerk[2:4]
产生列表[36, "123–45–6789"]
。在列表内的项目可以包括其他结构。Icon包括了list()
列表创建函数,用来建造更大的列表;例如vector := list(10, 0.0)
,生成10
个0.0
的一个列表。
集合类似于列表,但是只包含任何给定值的一个单一成员。Icon包括了++
来产生两个集合的并集,**
用于交集,和--
用于差集。可以通过用单引号包围字符串来建造Cset
,例如:
Icon包括一些预定义的Cset
,即包含各种字符的集合,它有四个标准Cset
:&ucase
、&lcase
、&letters
和&digits
。
表格是有序对的搜集,有序对被称为元素,它由键和对应的值组成。表格在其他语言中叫做关联数组、映射或字典。表格类似于列表,但是它的键或称为“下标”不必须为整数而可以是任何类型的值:
表格创建函数table()
建立了使用的0
作为任何未知键的缺省值的一个表格。接着向它增加了两个项目,具有键"here"
和"there"
,及其各自的值1
和2
。
搜集可以通过固有的生成器来遍历。例如针对文件的read()
、针对队列的get()
和针对堆栈的pop()
:
使用如前面例子中见到的失败传播,可以组合测试和循环:
前缀算子!x1
,其严格语法描述为!x1 : x2, x3, ..., xn
,从一个运算元x1
生成多个值x2, x3, ..., xn
:
- 如果
x1
是一个文件,!x1
生成x1
余下的诸行。 - 如果
x1
是一个字符串,!x1
生成x1
的一个字符的诸子串,并且如果x1
是变量则产生诸变量。 - 如果
x1
是一个列表、表格或记录,!x1
生成具有x1
诸元素的诸变量。产生次序,对于列表和记录是从开始至结束,而对于表格是不可预测的。 - 如果
x1
是一个集合,!x1
以不可预测的次序生成x1
的诸成员。
遍历搜集可以使使用叹号语法进一步简化:
在这种情况下,在write()
内的!lines
,导致从列表lines
一个接一个的返回一行文本,并且在结束处失败。而!&input
,类似于从标准输入文件&input
读取一行的read()
,一个接一个读取并返回诸行直到文件结束。
在Icon中,字符串是字符的列表,可以使用“叹号语法”来迭代:
这将一行一个的打印出字符串的每个字符。
子字符串提取
字符串是字符的列表,可以使用在方括号内的一个范围规定从字符串中提取出子字符串。子字符串范围规定,可以返回到一个单一字符的一个点,或字符串的一个分节(section)或分片(slice)。
字符串可以从左至右索引或者从右至左索引,并且在一个字符串内的位置被定义为在字符之间:1A2B3C4
或者−3A−2B−1C0
。例如:
"Wikipedia"[0]
会导致失败。这里最后例子采用了x1[i1+:i2] : x2
表达式,产生x1
在i1
和i1 + i2
之间的子字符串。
子字符串规定可以用作字符串内的左值。这可以被用来把字符串插入到另一个字符串,或删除字符串的某部分。例如:
字符串扫描
对处理字符串的进一步简化是“扫描”系统,通过?
来发起,它在一个字符串上调用函数:
Icon称呼?
的左手端为“主语”,并将它传递到字符串函数中。所调用的生成器函数find()
接受两个参数,查找的文本和要在其中进行查找的字符串。使用?
之时,第二个参数是隐含的,而不再由编程者来指定。多个函数被依次调用于一个单一字符串上,是一种常见情况,这种风格可以显著的缩减代码长度并增加清晰性。
?
不是简单的一种语法糖,它还为任何随后的字符串操作,建立一个“字符串扫描环境”。这基于了两个内部变量,&subject
和&pos
,这里的&subject
是要扫描的字符串,而&pos
是在这个主语字符串内的当前位置或“游标”。
表达式x ? expr : x
,保存当前的主语和位置,并接着分别设置它们为x
和1
,接着求值expr
;它的产出就是expr
的产出,在从expr
退出时它将主语和位置复原为保存的值。例如:
将产生:
subject=[this is a string] pos=[1]
内置和用户定义的函数,可以被用于在要扫描的字符串上移动。所有内置函数缺省采用&subject
和&pos
,来允许用上扫描语法。比如设置扫描位置的函数tab(i) : s
:产生子字符串&subject[&pos:i]
,并且将i
赋值到&pos
。下列例子代码,写出在一个字符串内,所有空白界定出的word
:
将产生:
this is a string
算子||
串接两个字符串,||:=
是一种增广赋值,a ||:= b
等价于a := a || b
。这个例子介入了一些新函数,pos(i1) : i2
用于测试扫描位置,如果&pos = i1
返回&pos
,否则失败。这里的循环以通过not
反转的pos(0)
作为条件,0
在Icon的字符串位置编码中指示行结束,如果当前扫描位置不是0
,pos()
返回&fail
。Icon不简单的直接使用&pos
,因为&pos
是一个变量,不能提供&fail
值,需要提供函数pos()
对&pos
做轻量级包装,从而便于使用Icon的目标导向控制流程。
many()
函数从当前&pos
开始,找到在所提供的Cset
参数中字符的最长序列之后的位置。在这例子中,它查找空格字符,所以这个函数的结果是在&pos
之后的第一个非空格字符的位置;接着用tab()
移动&pos
到那个位置。upto()
函数返回在所提供的Cset
中字符之前的位置,接着由另一个tab()
来设置&pos
。
这个例子通过可以使用更合适的“字分隔”Cset
,包括上句号、逗号和其他标点,还有其他空白字符如tab和不换行空格,从而变得更加健壮,还可以将其用于many()
和upto()
函数。
下面给出结合了生成器与字符串扫描的一个更复杂的例子:
将产生:
Mon Dec 8
初始化子句形式为initial expr
,其中的expr
是于所在函数首次被调用时求值的表达式。表达式=s1
等价于tab(match(s1))
。表达式*x
计算x
的大小。这里介入了内置函数match(s1,s2,i1,i2) : i3
,它匹配初始字符串:如果s1 == s2[i1+:*s1]
,产生i1 + *s1
,否则失败;它设定有缺省值:s2
为&subject
;i1
在s2
缺省时为&pos
,否则为1
;i2
为0
。
八皇后问题例子
解1
|
解2
|
算子|||
串接两个列表。可逆(reversible)赋值算子<-
,在被恢复的时候,上次暂停时它所设置的值被逆转回原来的值。不使用的<-
话,可以将这里的suspend col[c] <- …… <- c
替代为suspend col[c] := …… := c do col[c] := …… := 0
。
将这段代码保存入queens.icn
文件中,下面演示其执行结果并提取其92
个解中的前两个解:
$ icon ./queens.icn | wc -l
92
$ icon ./queens.icn | sed -n '1,2p'
a1,b7,c5,d8,e2,f4,g6,h3
a1,b7,c4,d6,e8,f2,g5,h3
参见
引用
参考书目
外部链接
Wikiwand - on
Seamless Wikipedia browsing. On steroids.