基于 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) //函数