【程序设计】8.3 [表一表] 序表生成

 

8.3 序表生成

为了进一步学习排列上的运算,我们需要一个记录数多一点的序表,比如前面用过的四个字段的序表:name、sex、weight、height,能产生上百条记录再来做实验就方便许多。

显然,手工填入很多记录的字段值实在太麻烦,我们使用程序来生成,字段的具体取值并不影响学习,随机生成就可以了。

用我们现在学过的代码技术已经会做这件事了。


A

B

C

1

=create(name,sex,weight,height)


2

for 100

=string(A2)

=if(rand()<0.5,"Male","Female")

3


=50+rand(50)

=1.5+rand(40)/100

4


>A1.insert(0,B2,C2,B3,C3)

5

=A1.len()


有意义的 name 比较麻烦,我们就用数字串替代了。C2、B3、C3 都容易理解,然后在 B4 将生成的字段段插入到 A1 创建的空序表中。代码执行完之后我们就能得到一个有 100 条记录的序表了。

这个代码没有错,但还是有点麻烦。我们学过循环函数,容易想到这种任务应该有一个循环函数来解决。但用来产生新序列的循环函数 100.(x) 只能返回序列,不是我们需要的。

SPL 提供了返回新序表的循环函数,上面代码可以简化成一句:


A

1

=100.new(string(~):name,if(rand()<0.5,"Male","Female"):sex,50+rand(50):weight,1.5+rand(40)/100:height)

有点长,写成两行了,结构化数据的相关语句经常会很长,这也是使用网格写代码的优势,它不会让某个格子里太长的代码写出格子而影响到右边和下边的代码阅读。

和 A.(x) 返回与 A 同长度的序列类似,A.new(…) 将返回记录数与 A.len() 相同的序表,也是针对 A 的每个成员生成一条记录,最后由这些记录构成一个序表返回。

new 函数有点不同的地方在于参数的写法。

可以明显地看出来,逗号分隔了每个字段,字段取值的计算式在冒号前面,字段名在冒号后面。这个冒号是 new 函数特别的规定吗?

是,也不是。

在接触结构化数据之后,我们会常常面临更复杂的函数参数。就比如这个 new 函数,要正确执行它,就需要知道待生成序表的每个字段取值计算式以及字段名称,而且字段取值计算式和字段名称是一对,不能错位。当前 new 现在这个情况,字段取值计算式和字段名称总是两个一组,这时如果仍然只用逗号分隔的书写方式,也还可以用参数的位置来确定对应关系,但已经看着有点晕了。如果每一组的参数个数是不确定的,那就根本没办法对应了。其实,new 函数确实允许不写字段名,它在很多情况下可以自动识别出一个合理的字段名,如果每次都要写上会是非常麻烦的事情。

这种情况,一种办法是使用集合作为参数,比如这个 new 可以写成:

=100.new({string(~),name},{if(rand()<0.5,"Male","Female"),sex},{50+rand(50),weight},{1.5+rand(40)/100,height})

这里的参数分成 4 组,每个字段是一组,每组有两个成员,分别是计算式和字段名,这样,参数间的对应关系就能被描述清楚,如果字段名不写,结果也就是某一组只有一个成员,不会和其它组的参数错位。

这就是多层次参数的概念,写在函数中的参数要分成多层的组,保证清晰地描述这些参数之间的对应关系。

理论上,可能还有更多的层次,这样 {} 还可能嵌套,这样看起来还是会有点乱。

SPL 没有采用嵌套 {} 的写法,而约定只支持三层参数,分别用分号、逗号和冒号来分隔。分号是第一级,分号隔开的参数是一组,这个组内如果还有下一层参数则用逗号分隔,再下一层参数则用冒号分隔。到此为止,没有更下一层了。

实践表明,三层基本够用了,很少用这种写法还很难描述清楚的参数关系了。而只有逗号则经常会发生描述不清的现象。

其实我们在讲 top 函数时已经见过分号了,但没有仔细剖析。

层次参数语法也是 SPL 的发明,它并非结构化数据计算所特有,在常规数值运算时也会涉及,但由于结构化数据处理的复杂性而经常出现。

如果读者学过 SQL,那么可以对比一下,SQL 用关键字分隔的各个部分也可以理解成层次参数,只不过伪装成英语会有更好的易读性,但通用性差很多,要为每个语句选择专门的关键字。结构化数据的复杂性就会迫使层次参数出现(形式各不同)。

理解了层次参数,上面的 new 函数就很容易看懂了。冒号确实是 new 函数参数需要的,因它有两层参数,但也不是刻意为 new 函数规定的,而是 SPL 中的通行规则。

序表的 insert 函数也有层次参数,在插入时可以指定字段名,没有在参数中涉及到的字段会在插入后填成空。

比如,我们要在前面那个已经有 4 条手工记录的序表中再追加 100 条刚才用 new 函数造的这种记录,但只把 weight 和 height 填上,其它空着。


A

B

C

D

5

=create(name,sex,weight,height)

=A5.record([A1:D4])

6

for 100

>A5.insert(0,50+rand(50):weight,1.5+rand(40)/100:height)

实际情况中的结构化数据的字段常常特别多,如果不允许缺省写法就会更麻烦。

和序列类似,序表的 insert() 也可以一次性批量插入多条记录:


A

B

C

D

5

=create(name,sex,weight,height)

=A5.record([A1:D4])

6

>A5.insert(0:100,50+rand(50):weight,1.5+rand(40)/100:height)

层次参数能够很方便用一种模式表达各种意图。

现在我们手上有了一个有 100 条记录的序表,每条记录对应一个人。我们想获得一个新序表,比这个序列多一个字段,存储这里每个人的 BMI 指标。

有了 new 函数后,这很容易:


A

1

=100.new(string(~):name,if(rand()<0.5,"Male","Female"):sex,50+rand(50):weight,1.5+rand(40)/100:height)

2

=A1.new(~.name:name,~.sex:sex,~.weight:weight,~.height:height,~.weight/~.height/~.height:bmi)

作为循环函数,A1.new() 中 ~ 是有定义的,表示循环序列时的当前成员,也就是序表 A1 的某个记录。那么我们就可以使用 ~.F 的语法引用记录 ~ 的字段了

但是,这样写出来的 A2 有点长。

SPL 规定,在针对序表或排列的循环函数中,可以直接用字段名来引用当前成员的字段,而不必写 ~.,也就是,name 就是 ~.name,sex 就是 ~.sex,…。而且,我们前面说了,new 函数有时可以省略写字段名,当前计算式和目标序表字段名相同的时候就可以省略。这样,A2 可以写成:


A

1

2

=A1.new(name,sex,weight,height,weight/height/height:bmi)

这样看起来就清晰多了,而且不容易出错了。

这种在后边追加字段的情况,在结构化数据处理中很常见。SPL 干脆提供了一个函数来处理,代码可以再简化写成:


A

1

2

=A1.derive(weight/height/height:bmi)

derive 函数会在原序表数据结构的基础上追加新字段,原序表的字段将全部照抄。

需要特别说明的是,derive 函数都会造一个新序表出来,并不会改变原序表,它会把原序列的字段及取值全部抄过来,原序表仍然在那里没有动。事实上,derive 函数可以针对排列执行,而不是一定要针对序表。

new 函数当然更是会新建,它甚至可以针对序列而非排列执行。

现在,我们可以把合并 Excel 问题恢复到之前的要求:在后面追加上文件名。


A

1

=directory@p("data/*.xlsx")

2

=A1.(file(~).xlsimport@t().derive(filename@n(A1.~):File))

3

>file("all.xlsx").xlsexport@t(A2.conj())

句子有点长,我们多写了一行,其实所有动作写到一句都可以。

对于每个文件读出来的序表,用 derive 函数在后面追加一个 File 字段,取值为当前文件名。需要注意的是,derive() 是循环函数,在其中运算式里直接写的 ~ 是指其针对的序表(排列)的当前记录,这里追加文件名时要用上一层的 ~,也就是 A1.~。

【程序设计】 前言及目录
【程序设计】8.2 [表一表] 序表与排列
【程序设计】8.4 [表一表] 循环函数