【数据蒋堂】第 21 期:常规遍历语法
遍历可以说是最基本的集合运算了,比如求和、计数、寻找最大最小值等聚合运算,按条件过滤集合、根据集合成员生成另一个新集合,也都是遍历运算。集合化语法要求我们能用很短的语句(经常就只有一句,而不是若干语句构成的一段程序)来描述大部分遍历运算,这样我们需要考查遍历运算中可能出现的各种常见情况,并设计出合理自洽的语法规则。
我们从简单到复杂来考查遍历运算中的可能情况,并讨论 SQL 语法在这方面的表现。
1. 直接针对集合成员运算
比如计算集合成员的合计。
这是最简单的情况,采用普通的函数语法风格就可以,将待遍历的集合作为参数获得返回值,比如 sum(A) 用于计算集成 A 成员的合计,当然也可以使用对象式的语法风格写成 A.sum()。
2. 引用集合成员
比如我们不是要计算集合成员的合计,而是要计算平方和,那么这个平方该如何描述?
这就会用到我们在谈集合化语法时提到的 lambda 语法。平方这个运算本质上是一个函数,在遍历过程中它以被遍历集合的当前成员作为参数,返回该参数的平方。而 lambda 语法允许将这个函数以表达式的形式并一起写入整个计算遍历运算式,一个语句就可以完成。但这里就有一个问题,我们在这个 lambda 表达式中用什么标识符或符号表示这个当前成员呢?
显然,象普通函数那个先定义参数名不是个好办法,那会让 lamdba 表达式写得很臃肿,失去 lambda 语法的简洁性。尽管有些程序设计语言确实是这么做的,不过我们并不提倡。使用一个固定的标识符也不好,太长了用起来不方便,太短又很可能与其它局部变量重名导致歧义。我们提倡在这里使用一个特殊符号来完成这个目的。
比如使用 ~ 表示当前成员时,平方和就可以写成 A.sum(~~),简单易懂。也可以分两步做,先计算出集合成员的平方构成一个新集合,再计算新集合的合计,写成类似 A.(~~).sum() 的形式,后一步不再需要 ~ 写法,前一步仍需要 ~ 写法来描述平方这个表达式函数。
3. 使用结构化数据时引用字段
但是,我们发现,被认为是集合化语言的 SQL 中并没有使用某个符号或标识符来表示当前遍历成员,那么 SQL 又是怎么解决问题 2 的呢?
事实上,SQL 并没有普通意义上可由任何成员构成的集合。SQL 的集合就是表,而表的成员都是相同结构的记录。SQL 体系中有记录这个概念,但并不能把记录作为一种数据类型来引用。如果我们要在 SQL 中针对一个单值成员的集合进行遍历,也只能把单值做成只有一个字段的记录,而针对这些记录构成的表进行遍历。所有计算都是针对某些字段进行的,而不能针对整条记录。
但这和 SQL 没有表示当前成员的符号有什么关系呢?
我们在前面说集合化语法时还提到,面向结构化数据计算的集合化语法需要有简洁的方式引用字段,SQL 提供了可以直接引用字段的便捷机制,而 SQL 又只能计算字段,那就可以不必再提供引用当前成员(记录)的手段了。比如 SQL 中计算平方和一定是某个字段的平方和,而整条记录(集合成员)的平方则没有意义。
SQL 牺牲了集合的表达能力而简化了语法。对于能够支持泛型成员构成集合的语言来讲,~ 写法就是必要的了。而且,如果用于结构化数据计算时,SQL 这种可以直接字段的写法也要得到支持才会方便,计算某销售帐目的金额时写成”~. 单价 *~. 数量”显然不如写成”单价 * 数量“更为简单直观,好的程序语言应当借鉴 SQL 这种风格。
4. 嵌套引用时的规则
遍历在本质上就是一个循环,而循环语句可能有多层,这样遍历也可能会有嵌套引用。比如计算 A,B 两个集合的交集,简单的算法就是遍历 A 的成员,看是不是在 B 集合中出现过(也是遍历),这就会涉及到两层的遍历。
这时候 ~ 写法就会产生歧义了,~ 到底是指 A 集合还是 B 集合的当前成员,这需要在语法规则上做一个明确的约定。
一般采用的是就近原则,即如果没有指明 ~ 是哪个集合的,那缺省认为是内层遍历集合的,而外层遍历集合的当前成员则需要显式地指出其从属于哪个集合。计算交集的表达式就可以写成 A.select(B.count(~==A.~)>0),其中的 ~ 缺省表示 B 的当前成员,而另一个要显式地写成 A.~ 以示区分。
面向结构化数据计算时可以直接引用字段名,这时也可能产生内外层的歧义,也可以适用于就近原则,SQL 就是这样。当内外层表有相同字段名时,则缺省被认为是内存表的字段,引用外层表的同名字段时必须显式地写上表名;如果内外存表中没有相同字段名,则可以正确识别出来而不必书写表名。
遍历运算虽然很基本,但设计其语法时仍有一些注意事项。SQL 在这方面总体表现不错,除了缺乏泛型成员的集合外,用于描述常规遍历运算还是比较方便简捷的。