第 21 期:常規遍歷語法
遍歷可以說是最基本的集合運算了,比如求和、計數、尋找最大最小值等聚合運算,按條件過濾集合、根據集合成員生成另一個新集合,也都是遍歷運算。集合化語法要求我們能用很短的語句(經常就只有一句,而不是若干語句構成的一段程式)來描述大部分遍歷運算,這樣我們需要考查遍歷運算中可能出現的各種常見情況,並設計出合理自洽的語法規則。
我們從簡單到複雜來考查遍歷運算中的可能情況,並討論 SQL 語法在這方面的表現。
-
直接針對集合成員運算
比如計算集合成員的合計。
這是最簡單的情況,採用普通的函式語法風格就可以,將待遍歷的集合作為引數獲得返回值,比如 sum(A) 用於計算整合 A 成員的合計,當然也可以使用物件式的語法風格寫成 A.sum()。
-
引用集合成員
比如我們不是要計算集合成員的合計,而是要計算平方和,那麼這個平方該如何描述?
這就會用到我們在談集合化語法時提到的 lambda 語法。平方這個運算本質上是一個函式,在遍歷過程中它以被遍歷集合的當前成員作為引數,返回該引數的平方。而 lambda 語法允許將這個函式以表示式的形式並一起寫入整個計算遍歷運算式,一個語句就可以完成。但這裡就有一個問題,我們在這個 lambda 表示式中用什麼識別符號或符號表示這個當前成員呢?
顯然,象普通函式那個先定義引數名不是個好辦法,那會讓 lamdba 表示式寫得很臃腫,失去 lambda 語法的簡潔性。儘管有些程式設計語言確實是這麼做的,不過我們並不提倡。使用一個固定的識別符號也不好,太長了用起來不方便,太短又很可能與其它區域性變數重名導致歧義。我們提倡在這裡使用一個特殊符號來完成這個目的。
比如使用 ~ 表示當前成員時,平方和就可以寫成 A.sum(
).sum() 的形式,後一步不再需要 ~ 寫法,前一步仍需要 ~ 寫法來描述平方這個表示式函式。
-
使用結構化資料時引用欄位
但是,我們發現,被認為是集合化語言的 SQL 中並沒有使用某個符號或識別符號來表示當前遍歷成員,那麼 SQL 又是怎麼解決問題 2 的呢?
事實上,SQL 並沒有普通意義上可由任何成員構成的集合。SQL 的集合就是表,而表的成員都是相同結構的記錄。SQL 體系中有記錄這個概念,但並不能把記錄作為一種資料型別來引用。如果我們要在 SQL 中針對一個單值成員的集合進行遍歷,也只能把單值做成只有一個欄位的記錄,而針對這些記錄構成的表進行遍歷。所有計算都是針對某些欄位進行的,而不能針對整條記錄。
但這和 SQL 沒有表示當前成員的符號有什麼關係呢?
我們在前面說集合化語法時還提到,面向結構化資料計算的集合化語法需要有簡潔的方式引用欄位,SQL 提供了可以直接引用欄位的便捷機制,而 SQL 又只能計算欄位,那就可以不必再提供引用當前成員(記錄)的手段了。比如 SQL 中計算平方和一定是某個欄位的平方和,而整條記錄(集合成員)的平方則沒有意義。
SQL 犧牲了集合的表達能力而簡化了語法。對於能夠支援泛型成員構成集合的語言來講,~ 寫法就是必要的了。而且,如果用於結構化資料計算時,SQL 這種可以直接欄位的寫法也要得到支援才會方便,計算某銷售帳目的金額時寫成”~. 單價~. 數量”顯然不如寫成”單價 數量“更為簡單直觀,好的程式語言應當借鑑 SQL 這種風格。
-
巢狀引用時的規則
遍歷在本質上就是一個迴圈,而迴圈語句可能有多層,這樣遍歷也可能會有巢狀引用。比如計算 A,B 兩個集合的交集,簡單的演算法就是遍歷 A 的成員,看是不是在 B 集合中出現過(也是遍歷),這就會涉及到兩層的遍歷。
這時候 ~ 寫法就會產生歧義了,~ 到底是指 A 集合還是 B 集合的當前成員,這需要在語法規則上做一個明確的約定。
一般採用的是就近原則,即如果沒有指明 ~ 是哪個集合的,那預設認為是內層遍歷集合的,而外層遍歷集合的當前成員則需要顯式地指出其從屬於哪個集合。計算交集的表示式就可以寫成 A.select(B.count(~==A.~)>0),其中的 ~ 預設表示 B 的當前成員,而另一個要顯式地寫成 A.~ 以示區分。
面向結構化資料計算時可以直接引用欄位名,這時也可能產生內外層的歧義,也可以適用於就近原則,SQL 就是這樣。當內外層表有相同欄位名時,則預設被認為是記憶體表的欄位,引用外層表的同名欄位時必須顯式地寫上表名;如果內外存表中沒有相同欄位名,則可以正確識別出來而不必書寫表名。
遍歷運算雖然很基本,但設計其語法時仍有一些注意事項。SQL 在這方面總體表現不錯,除了缺乏泛型成員的集合外,用於描述常規遍歷運算還是比較方便簡捷的。