【程序设计】5.3 [一把抓] 循环函数进阶

 

5.3 循环函数进阶

SPL 在循环函数中提供了充当循环变量的 ~ 符号,可以简化许多原本要用循环语句来写的代码。但我们知道,针对序列的循环语句还提供了获取循环序号的语法(在循环变量前加 #),那么循环函数里是不是也有类似的东西呢?

是的,简单地使用 #符号即可。


A

1

=[3,4,3,6,1,4]

2

=A1.sum(if(#%2==1,1,-1)*~)

A2 将计算出序列 A1 的奇数号成员之和与偶数号成员之和的差。

使用 #可以计算出两个长度相同序列对位相加的序列。


A

B

1

=[3,8,2,4,7]

=[9,2,6,1,0]

2

=A1.(~+B1(#))


A2 将计算出 [A1(1)+B1(1),…,A1(5)+B1(5)]。

序列 A 中依次存储了某公司 1-12 月的销售额,我们现在想知道最大的月增长额是多少,也就是某个成员减前一成员之差的最大值。这个可以用 #来实现。


A

1

=[123,345,321,345,546,542,874,234,543,983,434,897]

2

=A1.(if(#>1,~-A1(#-1),0)).max()

当 #>1 时,即不是 1 月,就可以计算本月比上月的增长额了。上月值可以用 A1(#-1) 获得,用本月值 ~ 相减就可得到增长额了,再取最大值即可。

在循环函数的计算中,引用相邻成员是很常见的动作。SPL 提供了专门的符号 ~[-1] 用来表示这里的 A1(#-1),代码可以简化:


A

1

=[123,345,321,345,546,542,874,234,543,983,434,897]

2

=A1.(if(#>1,~-~[-1],0)).max()

[-i] 表示向前 i 个成员,[i]则表示向后 i 个成员,也就是 A1(#+i)。与使用 A(i)获取序列成员不同,使用 [] 时,计算出来的序列超出序列范围并不会报错,而是会得到一个 null 值。

再尝试一个向后引用的例子,计算每月及其前后各一个月的销售额的平均值,即在第 n 月时,计算第 n-1 月、第 n 月和第 n+1 月这三个月的平均值,称为移动平均。1 月和 12 月只算两个月。


A

1

=[123,345,321,345,546,542,874,234,543,983,434,897]

2

=A1.(avg(~[-1],~,~[1]))

注意,不能简单地加起来除以 3,而要用 avg 函数,它会正确处理有空值的情况。

利用 ~[] 写法,我们还可以简化帕斯卡三角的计算:


A

B

1

5

=[[1],[1,1]]

2

for 3,A1+1

=B1(A2-1).(~+~[-1])

3


>B1=B1|[B2|1]

这里利用了 ~[-1] 越界时会得到 null 的约定。

~[] 写法还可以取出一个连续的子序列。比如刚才的数据中,我们想用每月的当月销售额序列计算每月的积累销售额,可以这样写:


A

1

=[123,345,321,345,546,542,874,234,543,983,434,897]

2

=A1.(~[:0].sum())

~[:0] 表示从序列头到当前行,即相当于 A1.to(#)。这种语法的完整写法是 ~[a:b],将取出 A1.to(#+a,#+b),a 省略则从头取,即 A1.to(#+b);b 省略则取到尾,即 A1.to(#+a,)。

刚才的平均也可以用这种取子序列的办法:


A

1

=[123,345,321,345,546,542,874,234,543,983,434,897]

2

=A1.(~[-1:1].avg())

和循环语句类似,循环函数也可能有多层嵌套的情况。比如我们计算两个序列中成员分别两两相乘之积的和。

用循环语句并不难写:


A

B

C

1

=[4,3,2,8,7]

=[9,2,6,1,0]

=0

2

for A1

for B1

>C1+=A2*B2

但如果用循环函数写,则会有个障碍:


A

B

1

=[4,3,2,8,7]

=[9,2,6,1,0]

2

=A1.sum(B1.sum(~*~))

这个写法显然不对,最里面的 ~*~ 会计算出一个平方,我们本意是希望由外层 A1 的 ~ 和内层 B1 的 ~ 相乘,但现在只有一个 ~ 符号时区分不了。

引入一个中间临时变量可以解决这个问题:


A

B

1

=[4,3,2,8,7]

=[9,2,6,1,0]

2

=A1.sum((a=~,B1.sum(a*~)) )

另外,SPL 还约定,在 ~ 前加变量名可以表示指定的 ~,而没有加变量名的 ~ 则表示最内层的 ~,这样就容易写了:


A

B

1

=[4,3,2,8,7]

=[9,2,6,1,0]

2

=A1.sum(B1.sum(A1.~*~))

A1.~ 表示外层 A1 的 ~,另一个 ~ 没有前导变量,则表示内层 B1 的 ~。

外层循环函数针对的是没有变量名的序列,就无法被引用了,这时候需要事先用一个变量赋值,人为给一个名字。


A

B

1

=to(9)

=A1.sum(to(~,9).sum(A1.~*~))

2

=9.sum(to(~,9).sum(~*~))


计算九九表中所有乘法项之和,需要给 to(9) 一个命名,才能在多层循环函数中区分出层次。而直接用 9.sum(…) 的写法则无法区分内外层,因为 9.~ 不是一个合法的计算式。

有时候内外层都是同一个序列,即使用了复制到了不同变量,仍然区分不了:


A

B

1

=[4,3,2,8,7]

=A1.sum(A1.sum(A1.~*~))

2

=A1

=A2.sum(A1.sum(A2.~*~))

3

=A1.(~)

=A3.sum(A1.sum(A3.~*~))

B1 的写法和直接写 sum(*) 是一样,无法区分内外层。而 B2 的写法,看起来能区分,但 A1 和 A2 其实是同一个对象(回顾一下上一章的内容),循环函数计算时, ~ 是记在这个序列上的,只是用不同的变量名仍然无法区分出内外层;要像 B3 这样写法,把 A1 复制成一个新的序列,A3 的 ~ 和 A1 的 ~ 才会不同,才能执行出正确结果。

对于 #也是同样的的规则,可以尝试理解下面的例子在计算什么:


A

B

1

=[3,8,2,4,7]

=[9,2,6,1,0]

2

=A1.max(B1.max(A1.~-~))


3

=A1.max(B1.max(A1(A1.#)-B1(#)))


3

=A1.max(B1.max(A1(#)-B1(A1.#)))


SPL 还有一个与 A.(x) 很像的函数 A.run(x),这个函数也会依次计算 x,但是仍返回 A,不返回这些 x 构成的序列。

这有什么用?

x 可以是任意的表达式,我们说过 = 也是合适的运算符,而在 run 的时候可以用 ~=…的写法来改变它自己。比如 A.(~=*) 将把 A 变成其成员平方的序列,这和 A.(*) 是不同的,后者会返回一个新序列,而前者在把原序列修改了。

这似乎还是没什么区别?新产生和在原序列上修改,对于后续的计算也没什么不同。

对于只用 ~ 计算时,确实是区别不大(后面讲到记录和序表时才会有较大区别)。但如果借助 [] 符号引用相邻的成员,结果就会不一样了。

比如 A.run(~=~[-1]+~),会把 A 变成累计值序列。而 A.(~[-1]+~)则不同,它不会计算出累积值,只会算出相邻值的和。因为在后者会产生新序列,而 ~[-1] 和 ~ 都是原来序列 A 的,在计算过程不会被改变;但前者在计算过程中并不产生新序列,~[-1] 会被上一轮计算改变,就会造成累积值的效果了。

A.run(~=~[-1]+~)和 A=A.([:0].sum()) 的结果是相同的,都能计算出累计值,但前者计算量要少得多,因为它是在上一轮基础上再计算的,而后者每次都要从头计算。

建议读者自己用代码尝试一下。

利用这个机制,我们可以再简化e的计算,抛弃对临时变量的使用,一个表达式搞定:

=1+20.run(~=~*if(#>1,~[-1],1)).sum(1/~)

其中的 20.run(~=~*if(#>1,~[-1],1))将计算出阶乘值序列 [1!,2!,…,20!],每轮循环前 ~[-1] 已经是上一轮的阶乘值了,再 *~ 就是本轮的阶乘值。对于第一轮循环,需要用 if 处理一下,因为 SPL 规定任何数乘以 null 的结果是 null,这和加法不同,需要特殊处理。

有了阶乘值序列,再继续做一步 sum 就可以了。

循环函数威力巨大,使用得当能写出来非常简洁且优美的代码。

【程序设计】 前言及目录

【程序设计】5.2 [一把抓] 循环函数

【程序设计】5.4 [一把抓] 迭代函数 *