Scala 的设计目标——Martin Odersky 访谈(二)

阅读数:3878 2015 年 9 月 6 日

话题:JavaScala语言 & 开发架构

Scala 是一种新兴的通用用途、类型安全的 Java 平台语言,结合了面向对象和函数式编程。它是洛桑联邦理工大学教授 Martin Odersky 的心血结晶。本访谈系列由多部分组成,由 Artima 网站的 Frank Sommers 和 Bill Venners 向 Martin Odersky 讨教 Scala。在第一部分Scala 起源中(点击查看《Scala 起源》中文翻译),Odersky 讲述了导致 Scala 诞生的那些历史。在本期中,Odersky 讨论 Scala 设计中的妥协、目标、创新和优势。

Scala 的设计妥协

Frank Sommers: 你曾提及,你想要创造一门语言,存活于 Java 生态系统,整合 Java 基础设施。为了达到上述目标,你让 Scala 做了哪些妥协来兼容 Java 平台呢?

Martin Odersky: 我们运气很好,不需要做太多妥协。说起来,我们做的妥协总体上还指不定是好事还是坏事呢。我们不得不做出的妥协之一:接受 Java 里的静态重载模型。我们可能曾经想要试验某些更激进的做法,比如多重方法(multi-methods),然而,在当时,多重方法的设计尚且无人充分探索过。可能时至今日恐怕也还没充分探索,甚至我都不能完全肯定它自始自终算不算个好东西。它曾经具备无限可能待人探索。但我们没去探索,因为我们要保持兼容 Java。

Scala 为人诟病的第二件妥协是同时支持特质(trait)和类。如果能设计得更干净些的话,完全可以只支持特质。前人已经做过这种干净的设计。但我们没有这样做,因为我们想保证 Scala 与 Java 能双向互操作。 我们希望有办法让 Java 代码可以很容易地调用 Scala 代码。然而特质无法自然的映射到 Java 中,因为 Java 中没有特质。因此,我们照搬了 Java 原有的类设计,因为我们需要 Scala 到 Java 的映射,以便让我们更容易支持双向互操作。

妥协之三,说到底更像是库的问题而非语言问题,即,我们想要去掉 null。null 是很多很多错误的罪魁祸首。我们本来可以设计一门语言,压根不允许 null 成为任何类型的候选值,因为 Scala 的 Option 类型就能完美替代 null。但是显然很多 Java 库会返回 null,我们总得有个办法来处理这些返回值嘛。

为设计 Scala 而选择战场

Bill Venners: 为了让 Scala 更容易被 Java 程序员所接受,你往 Scala 中加入了哪些东西?例如,有没有类似 Java 选用花括号语法的决策(Java 的花括号相比别的分块语法,能让 C 和 C++ 程序员感觉更温馨)?

Martin Odersky: 我们没有专门为市场因素而进行设计,不过我们确实想避免只为昭显另类而设计让程序员觉得奇怪的东西。举例来说,我觉得花括号用作分隔符相当靠谱,于是我就选择了花括号。我们本来也可以任性一点,选用 begin、end 或者其他什么符号,但我觉得,那并不见得会更好。

有一项 Scala 特性,最初设计得很纯粹,但我们后来就改掉了。开始时,我们用冒号等号表示赋值(就类似 Pascal、MODULA 和 Ada 那样)而用等号表示等于。许多编程理论家会认为这是正确的做事方式。赋值和等于含义不同,所以你就应该用不同的符号表示赋值。但是后来我找了些 Java 前程序员来做测试。给我的反馈是“哦,这个语言看起来挺有趣呀。但为什么你要用冒号等号?这是什么啊?”我解释说,它类似 Pascal。他们说,“现在我明白了,但我还是不明白你为什么一定得这样做。”然后我意识到,这不是我们非得坚持的东西。我们不想说: “我们的语言比别人好,是因为我们的赋值操作用了冒号等号,而不用等号。”这完全不是重点,程序员两种方式都可以适应嘛。因此,我们决定不在这些小地方挑战惯例,毕竟我们还想在其他方面与众不同呢。

Bill Venners: 我以前听你讲过一句俗语“选择战场”,今天你没讲这句话。基本上,你认为冒号等号并不那么重要,还有其他东西才是你在乎并为之抗争的。那么有哪些重要的事情呢?你想以什么方式来说服人们,改变他们的思考方式和编程方式?

Martin Odersky: 我们关心的第一件事,就是把函数式编程和面向对象编程尽可能干净的集成在一起。我们希望让函数成为一等公民,要有函数字面量、闭包。我们也希望拥有函数式编程的其他性质,比如类型、泛型、模式匹配。我们还希望整合函数式和面向对象两部分时,实现的方式能比在以前 Pizza 语言中更干净。这正是我们一开始就深度关注的事情。

后来,我们才发现,达成这个目标并不难,因为函数式语言都有一组固定的功能。这些功能已经研究和证明得很透彻了,所以问题只剩下如何以最佳方式整合进面向对象编程中。在 Pizza 中,我们尝试得很笨拙,而在 Scala 中,我认为我们达成了两者之间更为平滑的整合。但后来我们发现,在面向对象方面仍有很多事情有待开发。面向对象编程(至少在同静态类型系统扔到一起时)仍然是相当神秘的领域。有一些成果我们见过、用过,然而我们发现几乎所有的语言都做了很多妥协。

因此,开发 Scala 时,我们开始探索该如何才能让对象兼容特质混入(mixin composition),如何才能抽象出自我类型(self type),如何才能使用抽象类型成员(abstract type member),以及,如何才能把这一切搞到一起。至 Scala 诞生时,已经有几种学术语言,能用特殊方式、在某几个方面处理这些问题,但尚未有所谓主流语言能全频段覆盖般让上述所有功能都运转起来。最后事实证明,Scala 的主要创新在面向对象一面,而这才是我们真正关心的。

Scala 的面向对象创新

Bill Venners: 我想弄清楚,你能不能给出一张具体列表,涵盖你所认为的 Scala 面向对象创新之处。

Martin Odersky: 首先,我们想拥有一门纯粹的面向对象语言,其中的每个值都是对象,每个操作都是方法调用,每个变量都是对象成员。所以尽管我们并不喜欢静态成员,但我们仍然需要有某些东西来取代它们。为此我们创建单例对象的概念。即便如此,单例仍然是全局结构。所以,我们面临的挑战,就是如何尽可能少的使用单例,因为只要给定一个全局结构,就无法对它进行修改,也无法创建它的新实例。对它测试非常难;对它修改,无论以任何方式,也非常难。

因此,我们面临的挑战正是如何使复杂组件得以构建,却不引用静态成员或者全局变量。特别是我们还得处理组件间的递归依赖。有两个组件,A 和 B。A 使用 B、B 也使用 A。如何让他们发现对方,并让他们一起运作? 我们做的第一件事是混入概念。Java 只能继承单一父类。虽然可以继承一打抽象接口,但却不能包含代码。相比之下,Scala 既有类又有特质,而特质可以包含带实现的方法(即函数体),还可以包含字段。不像 Java 只有类才能实现接口,Scala 有混入功能,可以混合一个类和多个特质的定义内容。要做到这一点、准确找到最终编译出的类有哪些定义项,实现细节需要一种叫做线性化的手法。

因此,我们必须为 Scala 定义出线性化排序算法。这件事倒是搞定了,但随后的问题是,一处混入代码如何找到其他混入代码。要是它需要其他一些代码提供的服务。它该怎么指定服务方?要做到这一点,标准的面向对象方式要使用抽象成员。若只是抽象方法的话,这么做效果倒是不错,但我们还得处理变量呢。混入代码之间如何相互找到对方字段?更重要的是,我们不得不处理类型。因为我们一开始就支持类型嵌套。类型嵌套有点类似内部类,我觉得这个特性非常重要。你使用混入时,某个特质如何找出其他所有特质中定义的内部类并使用这些内部类呢?我们经过摸索(这个特性主要靠摸索而非设计),发现只要在自我类型上增加一层抽象就可以做到这一点。

那么,“自我类型之上的抽象”到底是什么?比如,类中出现的this引用到底是什么类型?你会说,“哦,就是这个类的类型吧。”大多数人可能都会这样说。实际上,并不存在什么压倒性的理由能让这一论断必然成立。this的类型也可以是别的东西。唯一成立的边界条件是在你真正创建某个类的实例时,你新创建的对象确实满足“this的类型就是类本身的类型”这一论断。而反例恰恰在你处理抽象方法时就会碰到。你可以在类中定义抽象方法,而不需要实现。当你创建类实例时,编译器会检查每个抽象方法是否都有实现。所以,自我类型之上的抽象,其实只是这种情况推广开来,让你可以处理抽象字段,以及更重要的抽象类型。

自我类型之上的抽象这一技术,非常值得探索。我们通过探索,在理论层面学习它们,建立了一套演算法。我们把这套理论命名为 nu-object 演算法(即νObj,第一个字母是希腊字母ν),发表于欧洲面向对象编程会议 2003(European Conference on Object-Oriented Programming in 2003,ECOOP 2003)。这个概念我们最初发现时只是一种技术手法,能简化演算法中的某些处理过程。但没过多久,我们想起,它可能有助于 Scala 语言中的某些东西。对于它有什么用,当时我们所知甚少,但我们决定把它加入 Scala 试试。由于这是我们自己的演算法,我们就做了决定。没过多久,我们发现它达成了一条基本原则:让每个特质可以声明,自己需要哪些成员从其他特质混入。这正是现在 Spring 等工具试图解决的问题,即所谓的依赖注入。但依赖注入只支持注入字段(也许还能注入方法),而我们还可以支持注入类型。而且,Scala 能够静态注入,而不需要在运行时注入。再者,Scala 还能为注入的内部类型保证类型安全。因此,在某种意义上,我们能做的事情要比目前 Spring 等工具所做的更进一步。

它对我有什么用?

Bill Venners: 在本系列访谈的稍后几期,我们再接着谈依赖注入。现在先不提依赖注入,本期访谈我们还有一个问题:为什么?你刚才“抽象”地谈论了自我类型。你先前提及,要选择的两大战场是:混合函数式和面向对象编程,以及面向对象的创新。如果我的工作就是用 Java 编程,在我实际在做的工作中,这些东西怎么能帮助我呢?我会得到什么样的具体好处?

Martin Odersky: 其一,我们所面临的挑战是,我们既想要函数式,又想要面向对象。我们很早就建立了一个观念,我们认为,不可变对象未来会非常非常重要。果然现在每个人都在谈论不可变对象,因为人们认为它们是解决多核计算机造成的并发问题的关键部分。大家都说,不管你做什么,你都需要试着尽量地让你的代码使用不可变对象。在 Scala 中,我们很早就做到了这一点。五、六年前,我们就开始努力思考不可变对象。实际上,在 Scala 发明以前,只要有大量的面向对象字段,就必然表示可变对象。对于他们来说,可变状态和对象是一码事:可变状态就是对象的本质组成部分。我们必须从对象的本质里除去可变性的概念。我们一定要做些事才能实现这一目标。

例如,对于一个标准的 Java 对象,你可以创建字段,通常是可变字段。接着你需要定义构造函数,接受参数、给字段赋值。这就是根植于每个 Java 类中的可变性概念。现在,Java 有 final 字段。这类字段不被视为可变字段,因为它们只在构造函数中赋值一次。但是,你仍然可以看到对它们赋值的运算符。我们希望有一个更干净的概念,不需要构造函数、赋值等东西。

我们最终在 Scala 中直接实现了带参数的类。你只要在类名后面写上参数列表,里面就是类的参数。额外的可变字段、以及对字段赋值的构造函数,这些概念在 Scala 中压根就不存在。这实际上也引发了一些我们需要解决的问题。其中之一是,如果你想要有多个构造函数,怎么办?我们必须为此定义某种语法和规则。除了主要构造函数之外,你还可以额外定义辅助构造函数。另一个问题是,如果稍后你想让参数作为字段以供访问,怎么办?你需不需要另外创建一个字段并给它赋值?或者能否把参数传到类中,就立刻成为大家都能访问的可用字段?所以我们必须为此创造语法。我相信,我们创造的语法也算是前所未有。一旦你发明了参数字段的语法,你就不得不考虑重载它们的规则,所以我们必须解决这些问题。最终,为了让这些功能运转,我们在面向对象领域发展出相当多的崭新基石。

Bill Venners: 这对我们 Java 程序员有什么益处?

Martin Odersky: 对你的好处是,你可以用更简洁的语法编写类。这种语法看起来与你以前用的可变状态截然不同。在 Scala 中编写类时通常更简单、简洁,而且不可变对象的处理也同样大大简化了。因为 Scala 是不可变编程的真正专业户。在 Scala 中,你仍然可以像 Java 一样处理可变对象,甚至可变对象也会比 Java 简洁,尽管 Scala 真正的亮点是在不可变对象上。这比你用 Java 时更自然、更简洁。

查看英文原文:The Goals of Scala's Design


感谢魏星对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。