历时 7 天,我把一万行 Scala 代码移植到了 Kotlin 上!

Posted CSDN

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了历时 7 天,我把一万行 Scala 代码移植到了 Kotlin 上!相关的知识,希望对你有一定的参考价值。

【CSDN编者按】去年,Google 宣布 Kotlin 正式成为 android 官方开发语言,由此引发了迁移 Kotlin 的一股热潮。在本文中,作者分享了他在七天内把代码从 Scala 移植到 Kotlin 的经过,以及从中吸取的经验教训。

历时 7 天,我把一万行 Scala 代码移植到了 Kotlin 上!

以下为译文:

上周出了几件事,所以我决定把postgresql-async从Scala移植到Kotlin。虽然现在还有好多缺失的部分,但alpha版已经可以用了在这篇文章中我想分享把代码从Scala移植到Kotlin的经过,以及从中吸取的经验教训,希望可以帮助其他开发者解决同样的问题。而且我也在继续努力,解决剩下的问题。


历时 7 天,我把一万行 Scala 代码移植到了 Kotlin 上!

首先我想解释一下为什么要移植?


在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十分相似,无论是功能还是语法——所以我们很想试试能不能把代码移植过去。


历时 7 天,我把一万行 Scala 代码移植到了 Kotlin 上!

怎么做?


在阅读下面的技术细节之前请访问下下面的函数库,然后请给加个星 :

https://github.com/jasync-sql/jasync-sql

转换本身包括两个主要步骤:

  • 自动逐行搜索替换脚本内容,节省一些无谓的打字时间;

  • 人工审核代码,修改所有编译错误,决定怎样进行转换,并改进脚本。


历时 7 天,我把一万行 Scala 代码移植到了 Kotlin 上!

脚本


脚本其实是一段非常简单无脑的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)
9catch (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则可能会更麻烦。


历时 7 天,我把一万行 Scala 代码移植到了 Kotlin 上!

经验教训,以及我做出的决定


我写这篇文章的目的就是记录下我做过的事情。一些文件仍然需要转换,同时项目中还有其他人,所以这篇文章会有用的。

下面的项目顺序不分先后,以后也可能会更新。

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

译者:弯月,责编:郭芮

历时 7 天,我把一万行 Scala 代码移植到了 Kotlin 上!

微信改版了,

想快速看到CSDN的热乎文章,

历时 7 天,我把一万行 Scala 代码移植到了 Kotlin 上!


征稿啦

如果你有优质的文章,或是行业热点事件、技术趋势的真知灼见,或是深度的应用实践、场景方案等的新见解,欢迎联系 CSDN 投稿,联系方式:微信(guorui_1118,请备注投稿+姓名+公司职位),邮箱(guorui@csdn.net)。

推荐阅读:



以上是关于历时 7 天,我把一万行 Scala 代码移植到了 Kotlin 上!的主要内容,如果未能解决你的问题,请参考以下文章

向文件中写入一万行数据

70万行代码历时20年,一名开发人员写出的史诗般的计算机程序

70 万行代码历时 20 年,一名程序员写出的史诗般的计算机程序

70 万行代码历时 20 年,一名程序员写出的史诗般的计算机程序

70 万行代码历时 20 年,一名程序员写出的史诗般的计算机程序

干货 | 编写可移植C/C++程序的一些要点