第三章 生成器
3.1 表达式是生成器
Icon和其他编程语言的最大区别在于:
在Icon中,表达式是生成器。表达式生成值的序列。
可以肯定的是,常量和变量只能生成单个值,但是有些语言结构可以生成多个值,有些则可能不生成任何值。
表达式通过回溯生成它们的值。要理解它是如何工作的,您需要了解表达式的求值顺序。
3.2 表达式求值顺序
一般来说,表达式是从左到右、从内层到外层求值的。
• 表达式的计算控制顺序是从左到右遍历表达式进行求值。
• 一旦子表达式求值完成,就求值包含它们的表达式。
看这个表达:
(a-b)+(c-d)*(e-f)
表达式将按以下顺序求值:
1. t1:=a-b
2. t2:=c-d
3. t3:=e-f
4. t4:=t2*t3
5. t5:=t1+t4
Icon 与其他常见编程语言的区别在于,在 Icon 中,其表达式求值往下的过程中,控制流可以:
• 回退到操作列表中可以生成更多值的操作,
• 从该操作再生成一个新的值,
• 然后继续,用新的值重新评估后面的操作。
3.3 Every
一个 every 表达式
表达式:every e1
every会创建一个上下文,在这个上下文中表达式e1会生成它所有的值。
3.4 To
操作符to可以生成一系列值
表达式:a to b,如:1 to 5
可以生成序列值1,2,3,4,5。
注意,需a大于等于b,不然无输出。
是否允许它生成所有这些值取决于它发生的上下文。有些上下文最多允许生成一个值。例如,在过程体中的表达式序列中,每个表达式最多只能生成一个值。因此,下面的代码将写出单个值10。
procedure main()
write(10 to 20)
end
但是,下面的代码将写出这一行序列
例6 Write 1 2 3 4 5
procedure main()
every writes(" ",1 to 5)
write()
end
to运算符关联到左边,例如:
every writes(" ",1 to 3 to 5)
会输出
1 2 3 4 5 2 3 4 5 3 4 5
也就是说,a to b to c这个结构,生成的序列是
首先生成a to c的序列,再生成a+1 to c的序列,……一直递增到b to c序列生成完,停止
to操作符不适用于大整数,即大于机器字的整数。
3.5 To-by
to生成器有一个更完整的形式,“e1 to e2 by e3”,e3表达步进值。如果步进值为负,需e1大于等于e2
3.6 元素生成:!x
“!”操作符做以下事情:
• 如果x是字符串,就会从此字符串第一个字符开始逐一输出
• 如果x是一个字符串变量,就可从此字符串变量第一个字符开始逐一赋值改变,如:
x:="abc"
i:=0
every (!x:=(i:=i+1))
变量x的值会变成“123”
• 如果x是一个列表,就会从此列表第一个元素开始逐一输出if x is a list, !x generates the elements of x in order from 1, i.e., x[1 to *x].
• 如果x是一个列表,赋值操作的话,就类似上面的对字符串变量赋值。并且,!x:=值 这个形式就是对列表每个元素都赋同一个值,如:!x:=0,就整体赋值为零了
• “!”操作符也适用于其他数据类型,将在有关这些类型的章节中讨论。
3.7 回溯
是如何工作的?以上面的“every (!x:=(i:=i+1))”为例
every类似于python里的next,不断让生成器往下走
如上,第一步x[1]:=?,表达式右边是(i:=i+1),计算后是1,就回溯赋值给了x[1]
every让!生成器往下走一步,x[2]:=?了,表达式(i:=i+1)计算后是2,就回溯赋值给了x[2]
当!x走完x[3],every让继续下一步,x没有下一个字符了,!x就会失败返回,也就这句结束运行
3.8 失败
有些控制结构,如every-expression,会导致回溯到表达式。
导致回溯的另一种方式是让表达式失败。为了更人性化一点,如果操作失败,控制回退,看看它是否能生成一些值,使操作成功。
有一个内置表达式总是失败“&fail”。每当Icon执行它,它总是回退。一个演示用&fail实现every-expression控制结构示例如下:
writes(" ",1 to 5) + &fail
如果只writes(" ",1 to 5),无every,那只会输出1就停了。后面“+”形成了一个表达式,加号后面跟一个只会失败的“&fail”,那么表达式内就回溯到writes,里面的1 to 5生成器就起作用了。
也就是说“+”只是为了writes这行形成表达式,并后跟失败来回溯的玩法。换成“-”、“*”等都可以,在这里并不发挥本来作用。
3.9 包含生成器的二元运算符
常规的二元操作符,如算术操作符,只是为一对操作数计算一个输出值。只有当生成器早先在操作列表中导致它们重新评估时,它们才会生成更多的值。
如果两个子表达式中都有生成器,则在允许左子表达式中的生成器生成另一个值之前,右子表达式中的生成器运行到完成。一旦左侧生成器生成了一个新值,右侧生成器将被重新初始化并重新启动。例如:
every writes(" ",(1 to 2) + (10 to 20 by 10))
会输出
11 21 12 22
也就是
1+10 1+20 2+10 2+20
3.10 算术关系运算符
关系运算符是一种二元运算符,如果操作数之间的关系成立,则成功;如果不成立,则失败。例如:
a < b #如果a的数值小于b,则成功,如果a的值大于或等于b,则失败。(如果操作数不是数字且不能转换为数字,则会停止程序的执行。)
如果关系操作符成功,则返回其右操作数的值。由于关系运算符是从左到右执行的,因此Icon允许使用如下方便的形式:
a < b < c #如果a小于b,b小于c,成功,得到c的值,否则失败
Icon的一个常见技巧是编写条件赋值语句。假设你想给变量x赋值x和y的最大值。你可以这样写:
x:= x < y
这表示:
• 如果x小于y,则将y的值赋给x。
• 如果x等于或大于y,则关系失败,不进行赋值,x保持原样。
3.11 e1 & e2
有一个特殊的二元操作符“&”,它只返回右操作数的值。它的优先级最低,甚至低于赋值。
“&”运算符用于控制而不是计算。它允许您编写一系列生成器和测试。例如:
every i:=1 to 3 & j:=1 to 3 & i~= j & writes(" ",i+j)
会输出
3 4 3 5 4 5
3.12 空和非空测试:/ x和\ x
/ x #变量是null就成功,非null就失败
\ x #变量是null就失败,非null就成功
因此,可以用于条件初始化,如
/ x := 100 #当变量还没赋值过,就初始化为100
注意,变量赋值“&null”(如x:=&null),跟没赋值过一样
另一种用法是表示布尔值。只允许&null表示false,其他任何东西表示true。在其他语言中,你可能会写成“if (x and y) then …”,你可以在Icon中写成“if \ x & \ y then …”。
注意:
"/ 表达式e" 完全等同于 "表达式e === &null"测试,但
"\ 表达式e"不等于"/ 表达式e"逆判断!
"\ 表达式e" 其表达式e失败的话,"\ 表达式e"也就失败,等同于判别表达式e为null。
"/ 表达式e" 其表达式e失败的话,"/ 表达式e"也就失败,等同于判别表达式e为非null。所以用法上,"/ 表达式e"不适合用在判别表达式,只适合判别变量。
3.13 表达式组求值
在一对圆括号内用逗号分隔的表达式列表的求值就好像它们被“&”分隔一样。例如:
every writes(" ",(1 to 3 , 1 to 2))
通常
(e1, e2, ..., en)
等同于
(e1 & e2 & ... & en)
但并不绝对
为什么做同一件事有两种方法?因为逗号分隔多表达式求值还有一种形式用法。
就是在括号前加上一个整数表达式,如:
k (e1,e2,...,ek,...,en)
在不影响整个表达式求值链的情况下,只返回括号中第k个表达式的返回结果。
例如:
every writes(" ", (1 to 2, 3 to 4, 5 to 6))
会输出
5 6 5 6 5 6 5 6
用python写就是
for i in range(1, 3):
for j in range(3, 5):
for k in range(5, 7):
print(k, end = ' ')
加上选择表达式用的数字
every writes(" ", 2 (1 to 2, 3 to 4, 5 to 6))
会输出
3 3 4 4 3 3 4 4
用python写就是
for i in range(1, 3):
for j in range(3, 5):
for k in range(5, 7):
print(j, end = ' ')
选择的一般形式是
e0 (e1,e2,...,ek,...,en)
其中表达式e0的值,是选择第几个表达式的值作为要返回的值。自然地,e0在其他表达式之前求值,它可以是一个生成器,例如:
every writes(" ", (1 to 2) (1 to 2, 3 to 4, 5 to 6))
会输出
1 1 1 1 2 2 2 2 3 3 4 4 3 3 4 4
分析下:
every writes(" ", 1 (1 to 2, 3 to 4, 5 to 6))
会输出
1 1 1 1 2 2 2 2
every writes(" ", 2 (1 to 2, 3 to 4, 5 to 6))
会输出
3 3 4 4 3 3 4 4
例子里e0是个生成器,生成(1, 2),那么结果是两个接续
输出
1 1 1 1 2 2 2 2 3 3 4 4 3 3 4 4
3.14 e1 | e2
表达式
e1 | e2
生成e1生成的所有值,然后再生成e2生成的所有值。
1 | 2 | 3 | 4
生成序列1,2,3,4。
操作符“|”的优先级比比较操作符低,所以可以这样写:
i = j | i = k
看i是否等于j或k,然而,还有一种更简洁的写法:
i = (j | k)
你可以在赋值语句的左侧使用every加“|”来对几个变量名一起赋值,例如:
every a | b | c := 0
等同于
a := b := c := 0
3.15 序列生成:seq(…)
to和to-by操作符需要指定起始值与结束值,生成的是有限序列。如果想要一个无限序列,可以使用系统函数seq来生成一个值序列。
seq() #生成序列1,2,3,...
seq(i) #生成序列i+1,i+2,...
seq(i,j) #生成序列i+j,i+2j,... 注意j不能为0
seq函数不适用于大整数。
3.16 生成重复序列:| e
如果把“|”放在一个生成器前面,如:
| g
它将允许g生成所有的值,然后它将重新初始化g并允许它再次生成所有的值,一次又一次。只有当g在初始化时立即失败时,才会导致g失败并允许控制回溯过去。
下面是一些例子:
| (1 to 3)
会输出
1, 2, 3, 1, 2, 3, 1, 2, 3 ...
|(0 | 1)
会输出
0, 1, 0, 1, 0, 1, ...
| 3 #放在非生成器前也可
会输出
3, 3, 3, ...
| &fail
会输出
失败了,啥也没输出
(i:=0) & |(i+:=1)
会输出
1, 2, 3, ...
先变量初始化为0,之后就反复自加一输出操作了
3.17 限制:e1 \ e2
如果您使用seq()或|e,会创建出无限序列。如果需要设定到一定数量截止,就用“\ e2”
将允许前面的生成器最多生成e2个值。如:
seq()\5
会输出
1, 2, 3, 4, 5
限制运算符“\”的优先级非常高,只比一元运算符低。
|1\5
会输出
1, 1, 1, 1, 1
(1 to 3)\(2 to 4)
会输出
1, 2, 1, 2, 3, 1, 2, 3
解析看
第一步“\2”,(1 to 3)会输出“1, 2”
第二步“\3”,(1 to 3)会输出“1, 2, 3”
第三步“\4”,限制上限是4个数,而(1 to 3)序列只三个数,所以会输出“1, 2, 3”
正如示例(1 to 3)\(2 to 4)所示,限制运算符右侧的表达式先于左边计算其操作数。
注意:这是Icon从左到右求值顺序的唯一例外。
3.18 章尾习题例子
根据勾股定理我们知道直角三角形两条边长度的平方和等于斜边长度的平方和。假设我们要找出1到100范围内的整数直角三角形的所有边长(包括斜边)。我们可以用下面的程序来做:
例7 直角三角形
procedure main()
every i:=1 to 100 &
j:=i to 100 &
m:=j to 100 &
m*m = i*i+j*j &
write(i," ",j, " ",m)
end