历时 7 天,我把一万行 Scala 代码移植到了 Kotlin 上!
【CSDN编者按】去年,Google 宣布 Kotlin 正式成为 Android 官方开发语言,由此引发了迁移 Kotlin 的一股热潮。在本文中,作者分享了他在七天内把代码从 Scala 移植到 Kotlin 的经过,以及从中吸取的经验教训。
以下为译文:
上周出了几件事,所以我决定把postgresql-async从Scala移植到Kotlin。虽然现在还有好多缺失的部分,但alpha版已经可以用了在这篇文章中我想分享把代码从Scala移植到Kotlin的经过,以及从中吸取的经验教训,希望可以帮助其他开发者解决同样的问题。而且我也在继续努力,解决剩下的问题。
1.首先我想解释一下为什么要移植?
在Outbrain我转到了一个新的团队,得到的任务之一就是负责将各种模块从Scala 2.10升级到2.11。这个任务是可行的,但十分痛苦,因为许多包都要求我们必须给所有JVM模块“打补丁”,就连Java模块都要!
由于所有模块都依赖于ob1k-db,而ob1k-db依赖于postgresql-async,后者又依赖于Scala 2.10和2.11下的不同的包。所以,可能更好的做法是干掉所有模块中对Scala的依赖……
而且上周,在经历了一年多的沉默后,终于有一个提交证实了postgres-sql不再提供维护了(https://github.com/mauricio/postgresql-async/commit/5716ac43818b6be0dc4fcc2b2655dde3411cdbe0)。这是压死骆驼的最后一根稻草。
而且,我们仍然在使用该函数库的MySQL异步风格的版本,而且还没有找到能代替它的东西。
但一个优势是Scala和Kotlin十分相似,无论是功能还是语法——所以我们很想试试能不能把代码移植过去。
2.怎么做?
在阅读下面的技术细节之前请访问下下面的函数库,然后请给加个星 :
https://github.com/jasync-sql/jasync-sql
转换本身包括两个主要步骤:
- 自动逐行搜索替换脚本内容,节省一些无谓的打字时间;
- 人工审核代码,修改所有编译错误,决定怎样进行转换,并改进脚本。
3.脚本
脚本其实是一段非常简单无脑的kscript代码(https://github.com/holgerbrandl/kscript),感觉都没必要贴出来。一些代码行甚至都没有替换成合法的语句(比如模式匹配和类型强制转换的部分)。
我没有时间也没有能力使用antlr(http://www.antlr.org/)之类的东西去写个语法分析器或完整的转换器,而且我还有一些非常特殊的需求。但你要是有兴趣的话可以试试。
话不多说,下面是脚本的简化版本:
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<String>): List<String> { 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<String>() 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<Byte>(", " ByteArray(") 35 .replace(" Array<Char>(", " 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<String>) { 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<String> = 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编写的(https://github.com/holgerbrandl/kscript),它接受一个参数:可以是扩展名已经改为.kt的Scala文件,也可以传递目录,如果是目录则该脚本会递归转换目录中的所有文件。
这个脚本会进行一些非常简单的逐行查找替换:def替换成fun,trait替换成interface,等等。没什么特别的东西。因为我前面说过,两者语法很相似,这一点起了很大作用。如果转换成Java则可能会更麻烦。
4.经验教训,以及我做出的决定
我写这篇文章的目的就是记录下我做过的事情。一些文件仍然需要转换,同时项目中还有其他人,所以这篇文章会有用的。
下面的项目顺序不分先后,以后也可能会更新。
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 → List
- IndexedSeq → List
- ArrayBuffer → 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的类型替换了,其中用到了一些扩展的帮助函数:
https://github.com/jasync-sql/jasync-sql/blob/master/db-async-common/src/main/java/com/github/jasync/sql/db/util/NullableUtils.kt
这里我发现Kotlin的方法更好,因为Scala有时使用Option,有时却直接使用null。
也可以用Java的Optional替换。
Version → KotlinVersion
其中有个专门的逻辑,但这个逻辑似乎很标准,所以我就使用KotlinVersion来替换了。
隐含转换
隐含转换是一切的邪恶之源(包括过早优化)。我发现我们的情况中可以很容易地使用扩展方法和Java静态方法来替换隐含转换。比如这里的第25行(https://github.com/mauricio/postgresql-async/blob/master/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/BigDecimalDecoder.scala)我们隐含地将ByteBuf转换成了ChannelWrapper,使用的是这里的第25行(https://github.com/mauricio/postgresql-async/blob/master/db-async-common/src/main/scala/com/github/mauricio/async/db/util/ChannelWrapper.scala)定义的方法。在Kotlin中,我们在ByteBuf上使用扩展函数(如这里:https://github.com/jasync-sql/jasync-sql/blob/master/db-async-common/src/main/java/com/github/jasync/sql/db/util/ByteBufExtensions.kt),并将ChannelWrapper变成了静态方法。
Traits → interface + 每个类的委托
似乎traits只是多重继承的替代品,因为它们有状态。我成功地用类委托(class delegation,第55行:https://github.com/jasync-sql/jasync-sql/blob/master/mysql-async/src/main/java/com/github/jasync/sql/db/mysql/MySQLConnection.kt)替换了它。缺点是这种实现要求方法抛出异常,所以如果没有被重载,那么运行时有可能会出错。见这里的第51行(https://github.com/jasync-sql/jasync-sql/blob/master/db-async-common/src/main/java/com/github/jasync/sql/db/pool/TimeoutScheduler.kt)。
以上,感谢阅读。欢迎大家指正!
原文:https://hackernoon.com/how-i-ported-10k-lines-of-scala-to-kotlin-in-one-week-c645732d3c1
作者:osha1
译者:弯月,责编:郭芮
“征稿啦”
CSDN 公众号秉持着「与千万技术人共成长」理念,不仅以「极客头条」、「畅言」栏目在第一时间以技术人的独特视角描述技术人关心的行业焦点事件,更有「技术头条」专栏,深度解读行业内的热门技术与场景应用,让所有的开发者紧跟技术潮流,保持警醒的技术嗅觉,对行业趋势、技术有更为全面的认知。
如果你有优质的文章,或是行业热点事件、技术趋势的真知灼见,或是深度的应用实践、场景方案等的新见解,欢迎联系 CSDN 投稿,联系方式:微信(guorui_1118,请备注投稿+姓名+公司职位),邮箱([email protected])。