【编者按】去年,Google 宣布 Kotlin 正式成为 Android 官方开发语言,由此引发了迁移 Kotlin 的一股热潮。在本文中,作者分享了他在七天内把代码从 Scala 移植到 Kotlin 的经过,以及从中吸取的经验教训。
以下为译文:
上周出了几件事,所以我决定把postgresql-async从Scala移植到Kotlin。虽然现在还有好多缺失的部分,但alpha版已经可以用了在这篇文章中我想分享把代码从Scala移植到Kotlin的经过,以及从中吸取的经验教训,希望可以帮助其他开发者解决同样的问题。而且我也在继续努力,解决剩下的问题。
在Outbrain我转到了一个新的团队,得到的任务之一就是负责将各种模块从Scala 2.10升级到2.11。这个任务是可行的,但十分痛苦,因为许多包都要求我们必须给所有JVM模块“打补丁”,就连Java模块都要!
由于所有模块都依赖于ob1k-db,而ob1k-db依赖于postgresql-async,后者又依赖于Scala 2.10和2.11下的不同的包。所以,可能更好的做法是干掉所有模块中对Scala的依赖……
而且上周,在经历了一年多的沉默后,终于有一个提交证实了postgres-sql不再提供文凭维护了。这是压死骆驼的最后一根稻草。
而且,我们仍然在使用该函数库的MySQL异步风格的版本,而且还没有找到能代替它的东西。
但一个优势是Scala和Kotlin十分相似,无论是功能还是语法——所以我们很想试试能不能把代码移植过去。
转换本身包括两个主要步骤:
自动逐行搜索替换脚本内容,节省一些无谓的打字时间;人工审核代码,修改所有编译错误,决定怎样进行转换,并改进脚本。
脚本其实是一段非常简单无脑的kscript代码,感觉都没必要贴出来。一些代码行甚至都没有替换成合法的语句(比如模式匹配和类型强制转换的部分)。
我没有时间也没有能力使用antlr之类的东西去写个语法分析器或完整的转换器,而且我还有一些非常特殊的需求。但你要是有兴趣的话可以试试。
话不多说,下面是脚本的简化版本:
1#!/usr/bin/env kscript
2
3import java.io.File
4
5// usage - one argument a .kt file (Scala file that was only renamed)
6// or a directory
7try {
8 main(args)
9} catch (e: Exception) {
10 e.printStackTrace()
11}
12
13fun convert(lines: List): List {
14 val methodNoBracsRegex=".fun\s+\w+\s+[:=].".toRegex()
15 val linesWithoutLicense=lines
16// The below lines just removed license comment
17// if (lines[0].startsWith("package "))
18// lines
19// else
20// lines.drop(15)
21 val result=mutableListOf()
22 linesWithoutLicense.forEach { lineBeforeConv ->
23 val convertedLine=lineBeforeConv
24 .replace("extends", ":")
25 .replace(" def ", " fun ")
26 .replace("BigInt(", "BigInteger(")
27 .replace("trait", "interface")
28 .replace("[", "<")
29 .replace("]", ">")
30 .replace("={", " {")
31 .replace(" new ", " ")
32 .replace(" Future<", " CompletableFuture<")
33 .replace(" Promise<", " CompletableFuture<")
34 .replace(" Array(", " ByteArray(")
35 .replace(" Array(", " CharArray(")
36 .replace("with", ",")
37 .replace("match", "when")
38 .replace("case class", "data class")
39 .replace("case _", "else")
40 .replace("case ", "")
41 .replace("=>", "->")
42 .replace(".asInstanceOf<", " as ") //manually fix >
43 .replace("final ", "")
44 .replace("fun this(", "constructor(")
45 .replace(" Seq<", " List<")
46 .replace(" IndexedSeq<", " List<")
47 .replace("<:", ":")
48 when {
49 convertedLine.startsWith("import ") -> {
50 val importsLines=if (convertedLine.contains("{")) {
51 val before=convertedLine.substringBefore("{")
52 convertedLine.substringAfter("{").substringBefore("}").split(",")
53 .map { "$before${it.trim()}" }
54 } else listOf(convertedLine)
55 importsLines.map { it.replace("_", "*") }.forEach {
56 result.add(it)
57 }
58 }
59 convertedLine.matches(methodNoBracsRegex) -> {
60 if (convertedLine.contains(":"))
61 result.add(convertedLine.replace(":", "():"))
62 else
63 result.add(convertedLine.replace("=", "()="))
64 }
65 else -> result.add(convertedLine)
66 }
67 }
68 return result
69}
70
71fun main(args: Array) {
72 val fileName=args[0]
73 if (fileName.endsWith(".kt")) {
74 workOnFile(fileName)
75 } else {
76 File(fileName).walk().forEach {
77 if (it.name.endsWith(".kt")) {
78 workOnFile(it.path)
79 }
80 }
81 }
82}
83
84fun readFileAsLinesUsingReadLines(fileName: String): List=File(fileName).readLines()
85
86fun workOnFile(fileName: String) {
87 if (!fileName.fileExists) {
88 println("WARN: file not exists $fileName")
89 return
90 }
91 println("working on $fileName")
92 val lines=readFileAsLinesUsingReadLines(fileName)
93 val fileContent=convert(lines).joinToString("
")
94 File(fileName).writeText(fileContent)
95}
这个脚本是用kscript编写的,它接受一个参数:可以是扩展名已经改为.kt的Scala文件,也可以传递目录,如果是目录则该脚本会递归转换目录中的所有文件。
这个脚本会进行一些非常简单的逐行查找替换:def替换成fun,trait替换成interface,等等。没什么特别的东西。因为我前面说过,两者语法很相似,这一点起了很大作用。如果转换成Java则可能会更麻烦。
我写这篇文章的目的就是记录下我做过的事情文件仍然需要转换,同时项目中还有其他人,所以这篇文章会有用的。
下面的项目顺序不分先后,以后也可能会更新。
Future → CompletableFuture
原来的代码大量使用了Scala的Future,所以我需要找个东西来代替。我有许多选择:
Netty future——似乎语法很复杂,而且已经过时。JavaRX/Guava/其他future库——需要额外的外部依赖。Java 8兼容的Future——至少需要依赖Java 8。Kotlin deferred——主要用于协程(coroutine),所以功能不太多,也不知道与Java用户的兼容性如何,对于我来说有点难度。
最后决定使用CompletableFuture作为主要的后端库。我觉得没必要在Android中使用响应式的relational-sql库,而且Java 8在Android之外的应用也非常广泛。
注意,CompletableFuture替换了Scala的Future和Promise。
依赖
由于这个项目类似于驱动程序,所以我尽量减少外部函数库的依赖,这个决定也影响了其他的决定。
Finalize
貌似在Kotlin中不需要覆盖finalize方法。
数据结构
有些我已经忘了,但我记得的转换有以下这些:
Seq → ListIndexedSeq → ListArrayBuffer → MutableList
位操作
Kotlin对于byte的处理有点奇怪,还不支持所有的操作符。一些类我转换成了Java,一些仍然保持Kotlin。希望我处理得没错,因为我并不十分确定Scala怎样处理这些操作。欢迎提意见。
扩展方法和属性
我一开始并不太理解,但后来意识到我可以使用扩展(extension)让Kotlin变得跟Scala相似,这一点非常酷。
例如Kotlin的List中有size,而Scala中叫做length。
这些问题都可以用扩展解决。
Try
我决定从Scala+Arrow移植一个相似的类使用。
方法定义和调用中的大括号
Scala并不强制大括号,所以有时转换会很痛苦。
Duration → Duration
决定使用java.util.Duration。
执行上下文和隐含参数
我发现这个功能非常混乱,所以我把所有隐含参数都改成了必须。虽然代码会变得冗余,但我觉得这样更清晰。
我使用common pool作为默认的执行上下文,尽管在ob1k中我们使用的是另一个。不管怎样,我们把它也改成了显式传递。
测试
原来的库使用了specs2。一开始我想暂时保留Scala的测试,但似乎这样做也需要很多工作,因为许多内部代码都改变了。测试的移植依然在进行中,主要工作都由贡献者们进行。
Option
大部分都用nullable的类型替换了,其中用到了一些扩展的帮助函数。
这里我发现Kotlin的方法更好,因为Scala有时使用Option,有时却直接使用null。
也可以用Java的Optional替换。
Version → KotlinVersion
其中有个专门的逻辑,但这个逻辑似乎很标准,所以我就使用KotlinVersion来替换了。
隐含转换
隐含转换是一切的邪恶之源(包括过早优化)。我发现我们的情况中可以很容易地使用扩展方法和Java静态方法来替换隐含转换。比如这里的第25行我们隐含地将ByteBuf转换成了ChannelWrapper,使用的是这里的第25行定义的方法。在Kotlin中,我们在ByteBuf上使用扩展函数,并将ChannelWrapper变成了静态方法。
Traits → interface + 每个类的委托
似乎traits只是多重继承的替代品,因为它们有状态。我成功地用类委托(class delegation)替换了它。缺点是这种实现要求方法抛出异常,所以如果没有被重载,那么运行时有可能会出错。见这里的第51行以上,感谢阅读。欢迎大家指正!