基于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,但缺乏元数据,不够专业。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可以用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,会被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。