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 的缺点在于要额外处理数组越界。
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 复杂。
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 那样单表计算时可以省略表名。
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 等类库读取,数据类型虽然不用在代码中解析,但要在配置文件中定义,实现过程不见得简单。
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 在解析数据类型时麻烦些,其他方面没有明显缺点。
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 表达式就变得难以阅读,还不如普通代码好理解;关联后的数据结构需要事先定义,灵活性差,影响解题流畅性。
val join=Orders.join(Employees,Orders("SellerId")===Employees("EId"),"Inner")
可以看到,Scala 不仅具备专用于结构化数据计算的对象和函数,而且可以很好地配合 Lambda 语言,代码更易理解,也不用事先定义数据结构。
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 也不支持定义索引,无法进行高性能随机查询,专业性还有缺陷。
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。
英文版