根据近期 Scala 路线图所公布的信息来看,Scala 从版本 2.12 开始,只能运行在 Java 8 及之后的版本上。InfoQ 找到了 Adriaan Moors(Typesafe 的 Scala 技术主管)和 Json Zaugg(Typesafe 工程师),了解到更多关于这个改动的内容,以及 Scala 将如何利用 Java 8 的 lambda 表达式的细节。
InfoQ:做出这个改动的最大驱动力是什么?
Adriaan:使用 Java 平台已经给 Scala 的成功和快速采用带来了帮助。我们热衷于和平台一起逐步发展,享受平台和平台生态系统带来的改进。对 lambda 表达式的原生支持使得 Java 8 的虚拟机成为了 Scala 更好的宿主。
InfoQ:当进行改动时,你认为最大的挑战可能是什么?
Adriaan:迁移到 Java 8 对于我们来说是一个很自然的演进。例如,Scala 2.11 已经有一个试验性的特性,这个特性是在 Java 6 上尽可能多地模仿 Java 8 提供的一些功能。
根据以往的实现,一个 Scala 的函数体会被提取到外部所在类的一个私有方法中。为了在运行时表示这个函数,会实例化一个匿名类。而这个类会包含一个方法,用于调用上述私有方法。通过迁移到 Java 8,我们不再需要在编译期生成这个匿名类,而是在运行时使用 LambdaMetaFactory 取而代之。类似地,2.11 的类型检查器支持从 Scala 的函数代码中合成单一抽象方法(Single Abstract Method)类(当使用参数 -Xexperimental 运行)。
这些是这个挑战的技术层面的亮点。正如你对一个平台变动的期望,社会层面也是一个非常重要的因素。2.12 的每一点的计划都围绕如何使升级变得容易。并且我们对社区抱有热切的期望。是否能快速采用 Scala 2.12 取决于围绕它的核心类库、测试框架、IDE 支持和其他工具的可用性。另外,我们意识到不是所有人都能立即升级到 Java 8。为了解决这个问题,我们计划把 2.11 和 2.12 在语言和类库上的差别控制在有限的范围内。我们想要开源作者们能够非常容易地交叉编译这两个版本。 这也是在更长一段时期内,要将 2.11 作为一个可用版本的一部分重要原因。关于我们打算如何执行这个策略,2.12 的路线图包含了更多的细节。
InfoQ:从最初发表的博客来看,由于触及到很多 Scala 子系统,这是个相当大的变动。你认为需要多大规模的团队才能完成这个变动?
Adriaan:很难用数字来表示“Scala 团队”的人数。和往常一样,社区将完成其中一部分重要的工作。一些 Typesafe 的工程师正在做 Scala 支持 Java 8 的工作。具体来说,Jason Zaugg 正在处理 Java 8 提供的 lambda 表达式;Lukas Rytz 正在跟进 Miguel Garcia 启动的工作,改工作是关于基于新的 ASM 的后台编译器和优化器的。所有的工作自然会贯穿于整个公司:Akka 和 Play 有易用的 Java 8 API,并且我们会不断改进它们。Scala IDE 和 sbt 已经支持 Java 8,我们的其他产品也同样支持。
InfoQ:Scala 具有 Java 8 的一些基本功能已经有一段时间了。在语义上 Scala 和 Java 8 是否存在着一些差别?你认为采用 JDK 8 进行开发时会遇到什么问题?
Jason:在一些少见的用例中,JDK8 中 LambdaMetaFactory 的语义并不能直接为我们工作。比如,它不知道如何对我们的 Value 类进行装 / 拆箱操作,或者如何通过 BoxedUnit 将一个返回值为空的方法句柄转换成 FunctionN::apply 的通用返回类型。但是,我们有一些选择来适应这些限制:在一些用例中,我们可以选择坚持使用当前的匿名类编码方式,或者我们可以生成访问器方法用以执行所需的装箱操作。根据原语生成正确的、指定版本的代码是一个需要认真对待的工作。但是,我们已经完成了这些挑战的解决方案的原型,并且非常确信不会有明显的障碍。
我们也计划利用默认的方法来编译 trait(Scala 中多重继承的轻量级形式)。这种编码方式将有更多的限制:当 trait 中方法覆盖了一个类中的某个方法时,trait 将无法正常工作;支持的字段也有这方面的限制。默认的方法在设计时并没有考虑到 Scala 这个应用,但它是设计二进制兼容性的重要资产。未来的 JDK 版本或许会提供更强大的工具; Brian Goetz 最近起草了关于 classdynamic 的建议,听起来对 Scala 有极大的好处。
InfoQ:对 JDK8 的 lambda 表达式子系统的整体设计有什么额外的评价吗?
Jason:JDK 中 lambda 表达式的实现看起来有着良好的构思,并得到了很好的执行。设计的关键是 invokedynamic(用于推迟处理编码的细节)的使用。这又是建立在 invokedynamic 本身前瞻性的规范上,它已经被测试证明是一个很强大的 API:已经多种方式成功地使用了该 API,这是连 API 作者都没有预见到的!即便如此,就 lambda 表达式通过 Java 语言所暴露出来的方式,对于 Scala 来说,还是有些不同的权衡的。最明显的不同就是使用 Functional 接口,而不是一个标准的拥有不同参数数量的 Function 泛型类型集合。这本身并不是坏事:为了支持 Functional 接口,我们也正在扩展我们的 Function 类。但是选择手动规范这些函数接口看起来在一定程度上是有特定目的的,不过这让编写通用代码变得更加困难。
InfoQ:Java 8 明显地引进了一些基本的函数式编程风格的语句(map,filter 等)。对于 Scala 来说,你认为这是一个威胁还是一个机会,或者说这是否会增加 Scala 开发者的数量?
Adriann:对于 Scala 来说,我完全相信这是一个非常好的机会。函数式编程不仅只是关于函数字面上的轻量级语义和多态方法调用时的一些类型推断。这只是个开始。对我而言,函数式编程是一个能通过构造合适大小的抽象来构建和理解程序的过程。Scala 提供了函数式编程的所有东西:从构造和定义单一方法的抽象(functions),到多个连贯一致的类(traits)。对我而言,一门(静态类型)函数式编程语言是:
1) 专注于构造短小、易于理解的功能单元。函数易于理解,因为它们只依赖于它们的参数。函数的类型和用于组合函数的组合器的类型刻画出了程序的高层结构。类型如同高科技管道:正确地引导数据至关重要,而容纳数据的结构上的改变不会对数据的处理造成阻碍。只有将管道隐藏在语言之下,这才能成真。Scala 强大的本地类型推断对于简单地使用一个函数式编程库起着至关重要的作用,因为这些库倾向于充分利用泛型。Java 8 有限的类型推断能够通过 IDE 的支持得到加强。但是,会导致要维护 IDE 生成类型的样板代码的老问题。
2) 具有一个可以让类库设计者简洁地表达这些安全的高阶抽象的类型系统。Scala 长期支持高阶的参数多态机制(“高种类的类型(higher-kinded types)”或者“类型构造器多态机制”),以及 ad-hoc 多态机制。Java 的泛型是第一阶的,并且重载是 ad-hoc 多态机制的一种极其有限的形式。相比获得的益处,这种形式带来了更多的痛苦。
3) 鼓励不变性。具有不可变性的代码通常更易于理解。当然,使用 Akka 对于扩展你的应用(使用机器的所有 CPU 或者跨数据中心)和排除故障是有显而易见的好处的。具体来说,可变性经常使得诊断你代码变中不同部分的牵连关系变得困难。Scala 也是非常务实的:有意地让 val 和 var 只有一个字母的差别(尽管 Scala IDE 将可变性变量高亮成红色)。
4) 根据数据的模式匹配定义函数(并且确保所有的场景都被覆盖),用不断演进的操作看待一个固定的数据模型;对于仅关注不变的功能和不断演进数据模型的 OOP(请参考访问者模式),Scala 进行了补充和深度地集成。
InfoQ:最后,我们的一个编辑有些无法接受 2.12 是一个大版本的事实。Scala 3 是否会有一些大的变化?
Adriaan:我们将要求使用者采取行动的升级分为两类:一类只需要重新编译(这类具有源码兼容性,但不具有二进制兼容性);另一类需要牵涉更多的工作,要求对源代码进行改动。我们保留了版本号中最有意义的部分(epoch)用于后一类的升级。这类升级很“罕见”(比如,每 10 年 1 次)。大约每 18 个月,我们会发布一个新的大版本。版本号的中间数值会增加。为确保平滑升级,相同的代码应该不需要修改就能够使用相邻的大版本进行编译-前提是“不赞成使用”警告(deprecation warnings)已经被处理。在我们的处理流程中,“不赞成使用”是个非常重要的部分:我们尽力(谨慎地)平衡稳定性和语言演进,不会轻易地破坏你的代码。但是我们确信,一种健康的,有节奏的创新将使每个人都受益。
最后,Scala 小版本的随意替换对于程序应该是无差别的,它们应该是向前和向后二进制兼容的。它们的发布节奏是多变的:在发布生命周期的早期,可能每隔一个月都会有一个小版本。随着我们把开发重心转移到下一个大版本,小版本的发布将减缓到每季度一次。
路线图中已讲明,我们计划使 2.11 和 2.12 两个大版本间的交叉构建至少像 2.10 和 2.11 间的交叉构建一样简单。当我们要在 Scala 3 中打破源码兼容性时,我们将会非常谨慎的处理,并且要有好的理由,比如使语言变得简洁,提高编译器的速度。
当从 Scala x.y.z 升级到 (x+1).y.z,你很可能需要修改源代码或者使用一个工具替你完成这个工作。当从 x.y.z 升级到 x.(y+1).z,在处理好源代码中的“不赞成使用”警告之后,你只需要重新编译源代码。因为“不赞成使用”的部分有可能在新版本中已经被删除了。最后,Scala x.y.a 和 x.y.b 应该能够替换使用。
被采访者
Adriann Moors 在 Typesafe 领导 Scala 团队。他从 2006 年开始使用 Scala 编程。当时他正在试验泛型编程,这充分利用了类型构造器的多态机制。 在Scala 实习期间,他在 Scala 2.5 中实现了这个特性(这使他成为 Scala 类型检查器的第一个外界贡献者)。毕业之后,Adriaan 作为一名博士后加入了 Scala 团队,专注于 Scala 的理论基础和实现(方法类型依赖和隐式搜索,类型构造器推断,2.10 中新的模式匹配器)。
Jason Zaugg是 Tpyesafe 的 Scala 团队中的一名软件工程师。过去 6 年里,他一直在企业项目和开源项目中使用 Scala 编写代码。目前专注于推动 Scala 平台的发展。
参考英文原文: Article: Scala 2.12 Will Only Support Java 8
感谢赵震一对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。
评论