JVM上數據處理語言的競爭:Kotlin, Scala 和 SPL

java識堂 發佈 2022-07-21T06:55:46.547214+00:00

基於 JVM 的開源數據處理語言主要有 Kotlin、Scala、SPL,下面對三者進行多方面的橫向比較,從中找出開發效率最高的數據處理語言。本文的適用場景設定為項目開發中常見的數據處理和業務邏輯,以結構化數據為主,大數據和高性能不作為重點,也不涉及消息流、科學計算等特殊場景。

基於 JVM 的開源數據處理語言主要有 Kotlin、Scala、SPL,下面對三者進行多方面的橫向比較,從中找出開發效率最高的數據處理語言。本文的適用場景設定為項目開發中常見的數據處理和業務邏輯,以結構化數據為主,大數據和高性能不作為重點,也不涉及消息流、科學計算等特殊場景。

基本特徵

適應面

Kotlin 的設計初衷是開發效率更高的 Java,可以適用於任何 Java 涉及的應用場景,除了常見的信息管理系統,還能用於 WebServer、Android 項目、遊戲開發,通用性比較好。Scala 的設計初衷是整合現代編程範式的通用開發語言,實踐中主要用於後端大數據處理,其他類型的項目中很少出現,通用性不如 Kotlin。SPL 的設計初衷是專業的數據處理語言,實踐與初衷一致,前後端的數據處理、大小數據處理都很適合,應用場景相對聚焦,通用性不如 Kotlin。

編程範式

Kotlin 以面向對象編程為主,也支持函數式編程。Scala 兩種範式都支持,面向對象編程比 Koltin 更徹底,函數式編程也比 Koltin 方便些。SPL 可以說不算支持面向對象編程,有對象概念,但沒有繼承重載這些內容,函數式編程比 Kotlin 更方便。

運行模式

Kotlin 和 Scala 是編譯型語言,SPL 是解釋型語言。解釋型語言更靈活,但相同代碼性能會差一點。不過 SPL 有豐富且高效的庫函數,總體性能並不弱,面對大數據時常常會更有優勢。

外部類庫

Kotlin 可以使用所有的 Java 類庫,但缺乏專業的數據處理類庫。Scala 也可以使用所有的 Java 類庫,且內置專業的大數據處理類庫(Spark)。SPL 內置專業的數據處理函數,提供了大量時間複雜度更低的基本運算,通常不需要外部 Java 類庫,特殊情況可在自定義函數中調用。

IDE 和調試

三者都有圖形化 IDE 和完整的調試功能。SPL 的 IDE 專為數據處理而設計,結構化數據對象呈現為表格形式,觀察更加方便,Kotlin 和 Scala 的 IDE 是通用的,沒有為數據處理做優化,無法方便地觀察結構化數據對象。

學習難度

Kotlin 的學習難度稍高於 Java,精通 Java 者可輕易學會。Scala 的目標是超越 Java,學習難度遠大於 Java。SPL 的目標就是簡化 Java 甚至 SQL 的編碼,刻意簡化了許多概念,學習難度很低。

代碼量

Kotlin 的初衷是提高 Java 的開發效率,官方宣稱綜合代碼量只有 Java 的 20%,可能是數據處理類庫不專業的緣故,這方面的實際代碼量降低不多。Scala 的語法糖不少,大數據處理類庫比較專業,代碼量反而比 Kotlin 低得多。SPL 只用於數據處理,專業性最強,再加上解釋型語言表達能力強的特點,完成同樣任務的代碼量遠遠低於前兩者(後面會有對比例子),從另一個側面也能說明其學習難度更低。

語法

數據類型

原子數據類型:三者都支持,比如 Short、Int、Long、Float、Double、Boolean

日期時間類型:Kotlin 缺乏易用的日期時間類型,一般用 Java 的。Scala 和 SPL 都有專業且方便的日期時間類型。

有特色的數據類型:Kotlin 支持非數值的字符 Char、可空類型 Any?。Scala 支持元組(固定長度的泛型集合)、內置 BigDecimal。SPL 支持高性能多層序號鍵,內置 BigDecimal。

集合類型:Kotlin 和 Scala 支持 Set、List、Map。SPL 支持序列(有序泛型集合,類似 List)。

結構化數據類型:Kotlin 有記錄集合 List<EntityBean>,但缺乏元數據,不夠專業。Scala 有專業的結構化數類型,包括 Row、RDD、DataSet、DataFrame(本文以此為例進行說明)等。SPL 有專業的結構化數據類型,包括 record、序表(本文以此為例進行說明)、內表壓縮表、外存 Lazy 游標等。

Scala 獨有隱式轉換能力,理論上可以在任意數據類型之間進行轉換(包括參數、變量、函數、類),可以方便地改變或增強原有功能。

流程處理

三者都支持基礎的順序執行、判斷分支、循環,理論上可進行任意複雜的流程處理,這方面不多討論,下面重點比較針對集合數據的循環結構是否方便。以計算比上期為例,Kotlin 代碼:


mData.forEachIndexed{index,it->

if(index>0) it.Mom= it.Amount/mData[index-1].Amount-1

}

Kotlin 的 forEachIndexed 函數自帶序號變量和成員變量,進行集合循環時比較方便,支持下標取記錄,可以方便地進行跨行計算。Kotlin 的缺點在於要額外處理數組越界。

Scala 代碼:

val w = Window.orderBy(mData("SellerId"))
mData.withColumn("Mom", mData ("Amount")/lag(mData ("Amount"),1).over(w)-1)

Scala 跨行計算不必處理數組越界,這一點比 Kotlin 方便。但 Scala 的結構化數據對象不支持下標取記錄,只能用 lag 函數整體移行,這對結構化數據不夠方便。lag 函數不能用於通用性強的 forEach,而要用 withColumn 之類功能單一的循環函數。為了保持函數式編程風格和 SQL 風格的底層統一,lag 函數還必須配合窗口函數(Python 的移行函數就沒這種要求),整體代碼看上去反而比 Kotlin 複雜。

SPL 代碼:

mData.(Mom=Amount/Amount[-1]-1)

SPL 對結構化數據對象的流程控制進行了多項優化,類似 forEach 這種最通用最常用的循環函數,SPL 可以直接用括號表達,簡化到極致。SPL 也有移行函數,但這裡用的是更符合直覺的「[相對位置]" 語法,進行跨行計算時比 Kotlin 的絕對定位強大,比 Scala 的移行函數方便。上述代碼之外,SPL 還有更多針對結構化數據的流程處理功能,比如:每輪循環取一批而不是一條記錄;某欄位值變化時循環一輪。

Lambda 表達式

Lambda 表達式是匿名函數的簡單實現,目的是簡化函數的定義,尤其是變化多樣的集合計算類函數。Kotlin 支持 Lambda 表達式,但因為編譯型語言的關係,難以將參數表達式方便地指定為值參數或函數參數,只能設計複雜的接口規則進行區分,甚至有所謂高階函數專用接口,這就導致 Kotin 的 Lambda 表達式編寫困難,在數據處理方面專業性不足。幾個例子:

"abcd".substring( 1,2)            //值參數
"abcd".sumBy{ it.toInt()}          //函數參數
mData.forEachIndexed{ index,it-> if(index>0) it.Mom=…}    //函數參數的函數帶多個參數

Koltin 的 Lambda 表達式專業性不足,還表現在使用欄位時必須帶上結構化數據對象的變量名(it),而不能像 SQL 那樣單表計算時可以省略表名。

同為編譯型語言,Scala 的 Lambda 表達式和 Kotlin 區別不大,同樣需要設計複雜的接口規則,同樣編寫困難,這裡就不舉例了。計算比上期時,欄位前也要帶上結構化數據對象變量名或用 col 函數,形如 mData ("Amount") 或 col("Amount"),雖然可以用語法糖彌補,寫成 $」Amount」或 'Amount,但很多函數不支持這種寫法,硬要彌補反而使風格不統一。

SPL 的 Lambda 表達式簡單易用,比前兩者更專業,這與其解釋型語言的特性有關。解釋型語言可以方便地推斷出值參數和函數參數,沒有所謂複雜的高階函數專用接口,所有的函數接口都一樣簡單。幾個例子:

mid("abcd",2,1)              //值參數
Orders.sum(Amount*Amount)          //函數參數
mData.(Mom=Amount/Amount[-1]-1)          //函數參數的函數帶多個參數

SPL 可直接使用欄位名,無須結構化數據對象變量名,比如:

Orders.select(Amount>1000 && Amount<=3000 && like(Client,"*S*"))

SPL 的大多數循環函數都有默認的成員變量 ~ 和序號變量 #,可以顯著提升代碼編寫的便利性,特別適合結構化數據計算。比如,取出偶數位置的記錄:

Students.select(# % 2==0)

求各組的前 3 名:

Orders.group(SellerId;~.top(3;Amount))

SPL 函數選項和層次參數

值得一提的是,為了進一步提高開發效率,SPL 還提供了獨特的函數語法。

有大量功能類似的函數時,大部分程序語言只能用不同的名字或者參數進行區分,使用不太方便。而 SPL 提供了非常獨特的函數選項,使功能相似的函數可以共用一個函數名,只用函數選項區分差別。比如,select 函數的基本功能是過濾,如果只過濾出符合條件的第 1 條記錄,可使用選項 @1:

T.select@1(Amount>1000)

對有序數據用二分法進行快速過濾,使用 @b:

T.select@b(Amount>1000)

函數選項還可以組合搭配,比如:

Orders.select@1b(Amount>1000)

有些函數的參數很複雜,可能會分成多層。常規程序語言對此並沒有特別的語法方案,只能生成多層結構數據對象再傳入,非常麻煩。SQL 使用了關鍵字把參數分隔成多個組,更直觀簡單,但這會動用很多關鍵字,使語句結構不統一。而 SPL 創造性地發明了層次參數簡化了複雜參數的表達,通過分號、逗號、冒號自高而低將參數分為三層:

join(Orders:o,SellerId ; Employees:e,EId)

數據源

數據源種類

Kotlin 原則上可以支持所有的 Java 數據源,但代碼很繁瑣,類型轉換麻煩,穩定性也差,這是因為 Kotlin 沒有內置的數據源訪問接口,更沒有針對結構化數據處理做優化(JDBC 接口除外)。從這個意義講,也可以說它不直接支持任何數據源,只能使用 Java 第三方類庫,好在第三方類庫的數量足夠龐大。

Scala 支持的數據源種類比較多,且有六種數據源接口是內置的,並針對結構化數據處理做了優化,包括:JDBC、CSV、TXT、JSON、Parquet 列存格式、ORC 列式存儲,其他的數據源接口雖然沒有內置,但可以用社區小組開發的第三方類庫。Scala 提供了數據源接口規範,要求第三方類庫輸出為結構化數據對象,常見的第三方接口有 XML、Cassandra、HBase、MongoDB 等。

SPL 內置了最多的數據源接口,並針對結構化數據處理做了優化,包括:

  • JDBC(即所有的 RDB)
  • CSV、TXT、JSON、XML、Excel
  • HBase、HDFS、Hive、Spark
  • Salesforce、阿里雲
  • Restful、WebService、Webcrawl
  • Elasticsearch、MongoDB、Kafka、R2dbc、FTP
  • Cassandra、DynamoDB、influxDB、Redis、SAP

這些數據源都可以直接使用,非常方便。對於其他未列入的數據源,SPL 也提供了接口規範,只要按規範輸出為 SPL 的結構化數據對象,就可以進行後續計算。

代碼比較

以規範的 CSV 文件為例,比較三種語言的解析代碼。Kotlin:

val File = File("D:\\data\\Orders.txt")
data class Order(var OrderID: Int,var Client: String,var SellerId: Int, var Amount: Double, var OrderDate: Date)
var sdf = SimpleDateFormat("yyyy-MM-dd")
var Orders=file.readLines().drop(1).map{
var l=it.split("\t")
var r=Order(l[0].toInt(),l[1],l[2].toInt(),l[3].toDouble(),sdf.parse(l[4]))
r
}var resutl=Orders.filter{
it.Amount>= 1000 && it.Amount < 3000}

Koltin 專業性不足,通常要硬寫代碼讀取 CSV,包括事先定義數據結構,在循環函數中手工解析數據類型,整體代碼相當繁瑣。也可以用 OpenCSV 等類庫讀取,數據類型雖然不用在代碼中解析,但要在配置文件中定義,實現過程不見得簡單。

Scala 專業性強,內置解析 CSV 的接口,代碼比 Koltin 簡短得多:

val spark = SparkSession.builder().master("local").getOrCreate()
val Orders = spark.read.option("header", "true").option("sep","\t").option("inferSchema", "true").csv("D:/data/orders.csv").withColumn("OrderDate", col("OrderDate").cast(DateType))
Orders.filter("Amount>1000 and Amount<=3000")

Scala 在解析數據類型時麻煩些,其他方面沒有明顯缺點。

SPL 更加專業,連解析帶計算只要一行:

T("D:/data/orders.csv").select(Amount>1000 && Amount<=3000)

跨源計算

JVM 數據處理語言的開放性強,有足夠的能力對不同的數據源進行關聯、歸併、集合運算,但數據處理專業性的差異,導致不同語言的方便程度區別較大。

Kotlin 不夠專業,不僅缺乏內置數據源接口,也缺乏跨源計算函數,只能硬寫代碼實現。假設已經從不同數據源獲得了員工表和訂單表,現在把兩者關聯起來:

data class OrderNew(var OrderID:Int ,var Client:String, var SellerId:Employee ,var Amount:Double ,var OrderDate:Date )

val result = Orders.map { o->var emp=Employees.firstOrNull{ it.EId==o.SellerId

}

emp?.let{ OrderNew(o.OrderID,o.Client,emp,o.Amount,o.OrderDate)

}

}

.filter {o->o!=null}

很容易看出 Kotlin 的缺點,代碼只要一長,Lambda 表達式就變得難以閱讀,還不如普通代碼好理解;關聯後的數據結構需要事先定義,靈活性差,影響解題流暢性。

Scala 比 Kotlin 專業,不僅內置了多種數據源接口,而且提供了跨源計算的函數。同樣的計算,Scala 代碼簡單多了:

val join=Orders.join(Employees,Orders("SellerId")===Employees("EId"),"Inner")

可以看到,Scala 不僅具備專用於結構化數據計算的對象和函數,而且可以很好地配合 Lambda 語言,代碼更易理解,也不用事先定義數據結構。

SPL 更加專業,結構化數據對象更專業,跨源計算函數更方便,代碼更簡短:

join(Orders:o,SellerId;Employees:e,EId)

自有存儲格式

反覆使用的中間數據,通常會以某種格式存為本地文件,以此提高取數性能。Kotlin 支持多種格式的文件,理論上能夠進行中間數據的存儲和再計算,但因為在數據處理方面不專業,基本的讀寫操作都要寫大段代碼,相當於並沒有自有的存儲格式。

Scala 支持多種存儲格式,其中 parquet 文件常用且易用。parquet 是開源存儲格式,支持列存,可存儲大量數據,中間計算結果(DataFrame)可以和 parquet 文件方便地互轉。遺憾的是,parquet 的索引尚不成熟。

val df = spark.read.parquet("input.parquet")

val result=df.groupBy(data("Dept"),data("Gender")).agg(sum("Amount"),count("*"))

result.write.parquet("output.parquet")

SPL 支持 btx 和 ctx 兩種私有二進位存儲格式,btx 是簡單行存,ctx 支持行存、列存、索引,可存儲大量數據並進行高性能計算,中間計算結果(序表 / 游標)可以和這兩種文件方便地互轉。


A

1

=file("input.ctx").open()

2

=A1.cursor(Dept,Gender,Amount).groups(Dept,Gender;sum(Amount):amt,count(1):cnt)

3

=file("output.ctx").create(#Dept,#Gender,amt,cnt).append(A2.cursor())

結構化數據計算

結構化數據對象

數據處理的核心是計算,尤其是結構化數據的計算。結構化數據對象的專業程度,深刻地決定了數據處理的方便程度。

Kotlin 沒有專業的結構化數據對象,常用於結構化數據計算的是 List<EntityBean>,其中 EntityBean 可以用 data class 簡化定義過程。

List 是有序集合(可重複),凡涉及成員序號和集合的功能,Kotlin 支持得都不錯。比如按序號訪問成員:

Orders[3]            //按下標取記錄,從0開始
Orders.take(3)            //前3條記錄
Orders.slice(listOf(1,3,5)+IntRange(7,10))    //下標是1、3、5、7-10的記錄

還可以按倒數序號取成員:

Orders.reversed().slice(1,3,5)        //倒數第1、3、5條
Orders.take(1)+Orders.takeLast(1)      //第1條和最後1條

涉及順序的計算難度都比較大,Kotlin 支持有序計集合,進行相關的計算會比較方便。作為集合的一種,List 擅長的功能還有集合成員的增刪改、交差合、拆分等。但 List 不是專業的結構化數據對象,一旦涉及欄位結構相關的功能,Kotlin 就很難實現了。比如,取 Orders 中的兩個欄位組成新的結構化數據對象。

data class CliAmt(var Client: String, var Amount: Double)
var CliAmts=Orders.map{it.let{CliAmt(it.Client,it.Amount) }}

上面的功能很常用,相當於簡單 SQL 語句 select Client,Amount from Orders,但 Kotlin 寫起來就很繁瑣,不僅要事先定義新結構,還要硬編碼完成欄位的賦值。簡單的取欄位功能都這麼繁瑣,高級些的功能就更麻煩了,比如:按欄位序號取、按參數取、獲得欄位名列表、修改欄位結構、在欄位上定義鍵和索引、按欄位查詢計算。

Scala 也有 List,與 Kotlin 區別不大,但 Scala 為結構化數據處理設計了更加專業的數據對象 DataFrame(以及 RDD、DataSet)。
DataFrame 是有結構的數據流,與資料庫結果集有些相似,都是無序集合,因此不支持按下標取數,只能變相實現。比如,第 10 條記錄:

Orders.limit(10).tail(1)(0)

可以想像,凡與順序相關的計算,DataFrame 實現起來都比較麻煩,比如區間、移動平均、倒排序等。
除了數據無序,DataFrame 也不支持修改(immutable 特性),如果想改變數據或結構,必須生成新的 DataFrame。比如修改欄位名,實際上要通過複製記錄來實現:

Orders.selectExpr("Client as Cli")

DataFrame 支持常見的集合計算,比如拆分、合併、交差合併,其中並集可通過合集去重實現,但因為要通過複製記錄來實現,集合計算的性能普遍不高。
雖然有不少缺點,但 DataFrame 是專業的結構化數據對象,欄位訪問方面的能力是 Kotlin 無法企及的。比如,獲得元數據 / 欄位名列表:

Orders.schema.fields.map(it=>it.name).toList

還可以方便地用欄位取數,比如,取兩個欄位形成新 dataframe:

Orders.select("Client","Amount")        //可以只用欄位名

或用計算列形成新 DataFrame:

Orders.select(Orders("Client"),Orders("Amount")+1000)    //不能只用欄位名

遺憾的是,DataFrame 只支持用字符串形式的名字來引用欄位,不支持用欄位序號或默認名字,導致很多場景下不夠方便。此外,DataFrame 也不支持定義索引,無法進行高性能隨機查詢,專業性還有缺陷。

SPL 的結構化數據對象是序表,優點是足夠專業,簡單易用,表達能力強。按序號訪問成員:

Orders(3)               //按下標取記錄,從1開始
Orders.to(3)              //前3條記錄
Orders.m(1,3,5,7:10)            //序號是1、3、5、7-10的記錄

按倒數序號取記錄,獨特之處在於支持負號表示倒數,比 Kotlin 專業且方便:

Orders.m(-1,-3,-5)            //倒數第1,3,5條
Orders.m(1,-1)              //第1條和最後1條

作為集合的一種,序表也支持集合成員的增刪改、交並差合、拆分等功能。由於序表和 List 一樣都是可變集合(mutable),集合計算時儘可能使用游離記錄,而不是複製記錄,性能比 Scala 好得多,內存占用也少。

序表是專業的結構化數據對象,除了集合相關功能外,更重要的是可以方便地訪問欄位。比如,獲得欄位名列表:

Orders.fname()

取兩個欄位形成新序表:

Orders.new(Client,Amount)

用計算列形成新序表:

Orders.new(Client,Amount*0.2)

修改欄位名:

Orders.alter(;OrderDate)          //不複製記錄

有些場景需要用欄位序號或默認名字訪問欄位,SPL 都提供了相應的訪問方法:

Orders(Client)               //按欄位名(表達式取)
Orders([#2,#3])              //按默認欄位名取
Orders.field(「Client」)       //按字符串(外部參數)
Orders.field(2)              //按欄位序號取

作為專業的結構化數據對象,序表還支持在欄位上定義鍵和索引:

Orders.keys@i(OrderID)            //定義鍵,同時建立哈希索引
Orders.find(47)              //用索引高速查找

計算函數

Kotlin 支持部分基本計算函數,包括:過濾、排序、去重、集合的交叉合併、各類聚合、分組匯總。但這些函數都是針對普通集合的,如果計算目標改成結構化數據對象,計算函數庫就顯得非常不足,通常就要輔以硬編碼才能實現計算。還有很多基本的集合運算是 Kotlin 不支持的,只能自行編碼實現,包括:關聯、窗口函數、排名、行轉列、歸併、二分查找等。其中,歸併和二分查找等屬於次序相關的運算,由於 Kotlin List 是有序集合,自行編碼實現這類運算不算太難。總體來講,面對結構化數據計算,Kotlin 的函數庫可以說較弱。

Scala 的計算函數比較豐富,且都是針對結構化數據對象設計的,包括 Kotlin 不支持的函數:排名、關聯、窗口函數、行轉列,但基本上還沒有超出 SQL 的框架。也有一些基本的集合運算是 Scala 不支持的,尤其是與次序相關的,比如歸併、二分查找,由於 Scala DataFrame 沿用了 SQL 中數據無序的概念,即使自行編碼實現此類運算,難度也是非常大的。總的來說,Scala 的函數庫比 Kotlin 豐富,但基本運算仍有缺失。

SPL 的計算函數最豐富,且都是針對結構化數據對象設計的,SPL 極大地豐富了結構化數據運算內容,設計了很多超出 SQL 的內容,當然也是 Scala/Kotlin 不支持的函數,比如有序計算:歸併、二分查找、按區間取記錄、符合條件的記錄序號;除了常規等值分組,還支持枚舉分組、對齊分組、有序分組;將關聯類型分成外鍵和主子;支持主鍵以約束數據,支持索引以快速查詢;對多層結構的數據(多表關聯或 Json\XML)進行遞歸查詢等。

以分組為例,除了常規的等值分組外,SPL 還提供了更多的分組方案:

枚舉分組:分組依據是若干條件表達式,符合相同條件的記錄分為一組。

對齊分組:分組依據是外部集合,記錄的欄位值與該集合的成員相等的分為一組,組的順序與該集合成員的順序保持一致,允許有空組,可單獨分出一組「不屬於該集合的記錄」。

有序分組:分組依據是已經有序的欄位,比如欄位發生變化或者某個條件成立時分出一個新組,SPL 直接提供了這類有序分組,在常規分組函數上加個選項就可以完成,非常簡單而且運算性能也更好。其他語言(包括 SQL)都沒有這種分組,只能費勁地轉換為傳統的等值分組或者自己硬編碼實現。

下面我們通過幾個常規例子來感受一下這三種語言在計算函數方式的差異。

排序

按 Client 順序,Amount 逆序排序。Kotlin:

Orders.sortedBy{it.Amount}.sortedByDescending{it.Client}

Kotlin 代碼不長,但仍有不便之處,包括:逆序正序是兩個不同的函數,欄位名必須帶表名,代碼寫出的欄位順序與實際的排序順序相反。

Scala:

Orders.orderBy(Orders("Client"),-Orders("Amount"))

Scala 簡單多了,負號代表逆序,代碼寫出的欄位順序與排序的順序相同。遺憾之處在於:欄位仍要帶表名;編譯型語言只能用字符串實現表達式的動態解析,導致代碼風格不統一。

SPL:

Orders.sort(Client,-Amount)

SPL 代碼更簡單,欄位不必帶表名,解釋型語言代碼風格容易統一。

分組匯總

Kotlin:

data class Grp(var Dept:String,var Gender:String) 
data class Agg(var sumAmount: Double,var rowCount:Int)
var result1=data.groupingBy{Grp(it!!.Dept,it.Gender)}
.fold(Agg(0.0,0),{acc, elem -> Agg(acc.sumAmount + elem!!.Amount,acc.rowCount+1)})
.toSortedMap(compareBy<Grp> { it.Dept }.thenBy { it.Gender })

Kotlin 代碼比較繁瑣,不僅要用 groupingBy 和 fold 函數,還要輔以硬編碼才能實現分組匯總。當出現新的數據結構時,必須事先定義才能用,比如分組的雙欄位結構、匯總的雙欄位結構,這樣不僅靈活性差,而且影響解題流暢性。最後的排序是為了和其他語言的結果順序保持一致,不是必須的。

Scala:

val result=data.groupBy(data("Dept"),data("Gender")).agg(sum("Amount"),count("*"))

Scala 代碼簡單多了,不僅易於理解,而且不用事先定義數據結構。

SPL:

data.groups(Dept,Gender;sum(Amount),count(1))

SPL 代碼最簡單,表達能力不低於 SQL。

關聯計算

兩個表有同名欄位,對其關聯並分組匯總。Kotlin 代碼:

data class OrderNew(var OrderID:Int ,var Client:String, var SellerId:Employee ,var Amount:Double ,var OrderDate:Date )

val result = Orders.map { o->var emp=Employees.firstOrNull{it.EId==o.EId}

emp?.let{ OrderNew(o.OrderID,o.Client,emp,o.Amount,o.OrderDate)}

}

.filter {o->o!=null}

data class Grp(var Dept:String,var Gender:String) 

data class Agg(var sumAmount: Double,var rowCount:Int)

var result1=data.groupingBy{Grp(it!!.EId.Dept,it.EId.Gender)}

.fold(Agg(0.0,0),{acc, elem -> Agg(acc.sumAmount + elem!!.Amount,acc.rowCount+1)})

.toSortedMap(compareBy<Grp> { it.Dept }.thenBy { it.Gender })

Kotlin 代碼很繁瑣,很多地方都要定義新數據結構,包括關聯結果、分組的雙欄位結構、匯總的雙欄位結構。

Scala

val join=Orders.as("o").join(Employees.as("e"),Orders("EId")===Employees("EId"),"Inner")
val result= join.groupBy(join("e.Dept"), join("e.Gender")).agg(sum("o.Amount"),count("*"))

Scala 比 Kolin 簡單多了,不用繁瑣地定義數據結構,也不必硬編碼。

SPL 更簡單:

join(Orders:o,SellerId;Employees:e,EId).groups(e.Dept,e.Gender;sum(o.Amount),count(1))

綜合數據處理對比

CSV 內容不規範,每三行對應一條記錄,其中第二行含三個欄位(即集合的集合),將該文件整理成規範的結構化數據對象,並按第 3 和第 4 個欄位排序.

Kotlin:

data class Order(var OrderID: Int,var Client: String,var SellerId: Int, var Amount: Double, var OrderDate: Date)
var Orders=ArrayList<Order>()
var sdf = SimpleDateFormat("yyyy-MM-dd")
var raw=File("d:\\threelines.txt").readLines()
raw.forEachIndexed{index,it->
if(index % 3==0) {
var f234=raw[index+1].split("\t")
var r=Order(raw[index].toInt(),f234[0],f234[1].toInt(),f234[2].toDouble(),
sdf.parse(raw[index+2]))
Orders.add(r)
}
}
var result=Orders.sortedByDescending{it.Amount}.sortedBy{it.SellerId}

Koltin 在數據處理方面專業性不足,大部分功能要硬寫代碼,包括按位置取欄位、從集合的集合取欄位。

Scala:

val raw=spark.read.text("D:/threelines.txt")
val rawrn=raw.withColumn("rn", monotonically_increasing_id())
var f1=rawrn.filter("rn % 3==0").withColumnRenamed("value","OrderId")
var f5=rawrn.filter("rn % 3==2").withColumnRenamed("value","OrderDate")
var f234=rawrn.filter("rn % 3==1")
.withColumn("splited",split(col("value"),"\t"))
.select(col("splited").getItem(0).as("Client")
,col("splited").getItem(1).as("SellerId")
,col("splited").getItem(2).as("Amount"))
f1.withColumn("rn1",monotonically_increasing_id())
f5=f5.withColumn("rn1",monotonically_increasing_id())
f234=f234.withColumn("rn1",monotonically_increasing_id())
var f=f1.join(f234,f1("rn1")===f234("rn1"))
.join(f5,f1("rn1")===f5("rn1"))
.select("OrderId","Client","SellerId","Amount","OrderDate")
val result=f.orderBy(col("SellerId"),-col("Amount"))

Scala 在數據處理方面更加專業,大量使用結構化計算函數,而不是硬寫循環代碼。但 Scala 缺乏有序計算能力,相關的功能通常要添加序號列再處理,導致整體代碼冗長。

SPL:


A

1

=file("D:\\data.csv").import@si()

2

=A1.group((#-1)\3)

3

=A2.new(~(1):OrderID,(line=~(2).array("\t"))(1):Client,line(2):SellerId,line(3):Amount,~(3):OrderDate )

4

=A3.sort(SellerId,-Amount)

SPL 在數據處理方面最專業,只用結構化計算函數就可以實現目標。SPL 支持有序計算,可以直接按位置分組,按位置取欄位,從集合中的集合取欄位,雖然實現思路和 Scala 類似,但代碼簡短得多。

應用結構

Java 應用集成

Kotlin 編譯後是字節碼,和普通的 class 文件一樣,可以方便地被 Java 調用。比如 KotlinFile.kt 里的靜態方法 fun multiLines(): List<Order>,會被 Java 正確識別,直接調用即可:

java.util.List result=KotlinFileKt.multiLines();
result.forEach(e->{System.out.println(e);});

Scala 編譯後也是字節碼,同樣可以方便地被 Java 調用。比如 ScalaObject 對象的靜態方法 def multiLines():DataFrame,會被 Java 識別為 Dataset 類型,稍做修改即可調用:

org.apache.spark.sql.Dataset df=ScalaObject.multiLines();
df.show();

SPL 提供了通用的 JDBC 接口,簡單的 SPL 代碼可以像 SQL 一樣,直接嵌入 Java:

Class.forName("com.esproc.jdbc.InternalDriver");
Connection connection =DriverManager.getConnection("jdbc:esproc:local://");
Statement statement = connection.createStatement();
String str="=T(\"D:/Orders.xls\").select(Amount>1000 && Amount<=3000 && like(Client,\"*s*\"))";
ResultSet result = statement.executeQuery(str);

複雜的 SPL 代碼可以先存為腳本文件,再以存儲過程的形式被 Java 調用,可有效降低計算代碼和前端應用的耦合性。

Class.forName("com.esproc.jdbc.InternalDriver");
Connection conn =DriverManager.getConnection("jdbc:esproc:local://");
CallableStatement statement = conn.prepareCall("{call scriptFileName(?, ?)}");
statement.setObject(1, "2020-01-01");
statement.setObject(2, "2020-01-31");
statement.execute();

SPL 是解釋型語言,修改後不用編譯即可直接執行,支持代碼熱切換,可降低維護工作量,提高系統穩定性。Kotlin 和 Scala 是編譯型語言,編譯後必須擇時重啟應用。

交互式命令行

Kotlin 的交互式命令行需要額外下載,使用 Kotlinc 命令啟動。Kotlin 命令行理論上可以進行任意複雜的數據處理,但因為代碼普遍較長,難以在命令行修改,還是更適合簡單的數字計算:

>>>Math.sqrt(5.0)
2.236.6797749979

Scala 的交互式命令行是內置的,使用同名命令啟動。Scala 命令行理論上可以進行數據處理,但因為代碼比較長,更適合簡單的數字計算:

scala>100*3

rest1: Int=300

SPL 內置了交互式命令行,使用「esprocx -r -c」命令啟動。SPL 代碼普遍較短,可在命令行進行簡單的數據處理。

(1): T("d:/Orders.txt").groups(SellerId;sum(Amount):amt).select(amt>2000)

(2):^C

D:\raqsoft64\esProc\bin>Log level:INFO

1       4263.900000000001

3       7624.599999999999

4       14128.599999999999

5       26942.4

通過多方面的比較可知:對於應用開發中常見的數據處理任務,Kotlin 因為不夠專業,開發效率很低;Scala 有一定的專業性,開發效率比 Kotlin 高,但還比不上 SPL;SPL 語法更簡練,表達效率更高,數據源種類更多,接口更易用,結構化數據對象更專業,函數更豐富且計算能力更強,開發效率遠高於 Kotlin 和 Scala。

SPL下載地址:http://c.raqsoft.com.cn/article/1595816810031

SPL開源地址:https://github.com/SPLWare/esProc

關鍵字: