动态语言已死?

Published: 19 Dec 2014 Category: 其它

真相总会不期而遇。它们总是不经意间降临,譬如当我读到这条微博的时候:

这是关于Facebook的Flow的一个很不错的讨论 – http://t.co/5KTKakDB0w

— David J. Pearce (@whileydave) November 23, 2014

David是Whiley编程语言的作者,这个语言内建了许多静态类型检查的特性,虽然比较小众,但粉丝也不少。它的一个很有意的特性就是流敏感(flow sensitive)类型(有时也被称为流类型),当它与联合(union)类型配合使用的时候会比较有用。下面是从它的使用向导中摘录的一个例子:

function indexOf(string str, char c) => null|int:

function split(string str, char c) => [string]:
  var idx = indexOf(str,c)

  // idx has type null|int
  if idx is int:

    // idx now has type int
    string below = str[0..idx]
    string above = str[idx..]
    return [below,above]

  else:
    // idx now has type null
    return [str] // no occurrence

记住了,像Ceylon这样的语言也支持流敏感类型,甚至是Java在也在一定程度上也是支持的,因为Java也有联合类型!

try {
    ...
}
catch (SQLException | IOException e) {
    if (e instanceof SQLException)
        doSomething((SQLException) e);
    else
        doSomethingElse((IOException) e);
}

Java的流敏感类型是显式且拖沓的。我们当然希望编译器能推导出所有的类型。像下面这么写的话也会进行类型检查并且能够通过编译就好了:

try {
    ...
}
catch (SQLException | IOException e) {
    if (e instanceof SQLException)
        // e is guaranteed to be of type SQLException
        doSomething(e);
    else
        // e is guaranteed to be of type IOException
        doSomethingElse(e);
}

流类型或者说流敏感类型指的是编译器可以从当前程序的控制流中推导出唯一可能的类型。它是在像Ceylon这样的现代语言中才出现一个相对较新的概念,它使得静态类型变得异常强大,尤其是当语言本身能支持通过var或者val关键字来进行复杂的类型推导的时候。

配备了Flow之后的静态类型的JavaScript

我们回到David的那条微博并看一下这篇文章对Flow是如何评价的:

http://sitr.us/2014/11/21/flow-is-the-javascript-type-checker-i-have-been-waiting-for.html

由于length的取值可能为空,因此Flow就不得不在函数体内判断它是否为空值。下面是进行了类型检查的版本:

function length(x) {
  if (x) {
    return x.length;
  } else {
    return 0;
  }
}

var total = length('Hello') + length(null);

Flow能够推导出在if体内x不能为空。

这相当巧妙。微软的TypeScript中也有一个类似的新特性。但Flow与TypeScript不同(至少它是这么声称的)。从官方的Flow介绍中可以看到Facebook Flow的本质所在:

Flow的类型检查是可选的——你不再需要在代码中到处进行类型检查了。然而,Flow的底层设计是基于这么一个假设的,即大多数JavaScript的代码其实都是静态类型的;尽管很多时候代码中并没有出现明确的类型,但在开发人员的脑海中它是实际存在的。Flow会尽可能地自动推导出这些类型,这意味着你无需修改代码便能找出里面的类型错误。换句话说,一些严重依赖于反射的JavaScript代码,尤其是框架的代码,通常很难进行静态检查。对于这种骨子里就是动态类型的代码,类型检查就变得不太准确了,因此Flow提供了一种简单的方式来显式地将这些代码置为是可信的,并忽略它们。这个设计在Facebook海量的JavaScript代码库中得到了验证:许多代码都默认归到了静态类型的分类,开发人员无需显式标注这些代码的类型便能找出其中类型错误的问题。

个中三昧

绝大多数JavaScript代码都是隐式的静态类型的

再进一步

JavaScript代码都是隐式的静态类型的

没错!

类型系统深受程序员的喜爱。他们喜欢正式地声明数据的类型,使得这些数据处于一个比较窄的约束下,这样才能确保程序的正确性。这正是静态类型的本质所在:一个设计良好的数据类型更不容易出错。

人们也喜欢将自己的数据结构以一种规范的形式存储到数据库中,这就是为什么SQL如此大行其道的原因,而无schema的数据库的市场份额始终上不去。这其实本质上都是一样的。在无schema的数据库中,其实你的脑子里还是存在一个schema,只是没有进行类型检查而已,并增加了确保程序正确性的负担。

还有一点需要注意的:一些NoSQL的厂商拼了命地在发表这些荒谬不堪的言论,告诉你,其实你根本就不需要schema,其实只是为了给自己的产品找定位,不过这种营销的伎俩很容易识破。无schema与动态类型的真正需求其实,都相当的少。换言之,你上一次在Java程序中通过反射来调用方法是什么时候的事了?很少用到吧。

不过有一样东西是以前静态类型语言所不具备而动态类型语言能做到的:避免代码冗长。这是因为虽然程序员钟爱类型系统以及类型检查,但他们并不喜欢去敲这些代码。

代码冗长才是问题所在,而非静态类型

来看一下Java进化的历史吧:

Java 4

List list = new ArrayList();
list.add("abc");
list.add("xyz");

// 为什么我需要这个Iterator呢?
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
    // 好吧,我清楚我这里就是String类型,为啥还要类型转换?
    String value = (String) iterator.next();

    // [...]
}

Java 5

// 居然得声明两次泛型!
List<String> list = new ArrayList<String>();
list.add("abc");
list.add("xyz");

// 比之前好多了,不过还是得声明是String类型
for (String value : list) {
    // [...]
}

Java 7

// 进步了点
List<String> list = new ArrayList<>();
list.add("abc");
list.add("xyz");

for (String value : list) {
    // [...]
}

    // [...]
}

Java 8

// 终于进化成了这样,虽然姗姗来迟了
Stream.of("abc", "xyz").forEach(value -> {
    // [...]
});

顺便提一下,当然了,上述这个功能其实用Arrays.asList()就能完成了。

Java 8还远谈不上完美,但至少是日臻完美了。现在在lambda参数列表中不用声明类型的原因是编译器替我们完成了推导,这点是相当重要的。

看一下Java 8之前类似于这个lambda的话要怎么写(假设那会儿已经有Stream了):

// Yes, it's a Consumer, fine. And yes it takes Strings
Stream.of("abc", "xyz").forEach(new Consumer<String>(){
    // And yes, the method is called accept (who cares)
    // And yes, it takes Strings (I already say so!?)
    @Override
    public void accept(String value) {
        // [...]
    }
});

现在我们拿Java 8跟JavaScript的版本作一个比较:

["abc", "xyz"].forEach(function(value) {
    // [...]
});

在简洁性方面它几乎已经达到和函数式,动态类型的JavaScript一样的水准了,唯一的不同之处就在于,我们(以及编译器)知道这个value的类型是String。还有就是我们知道forEach方法的确存在。我们还知道的是forEach方法接受一个带单个参数的函数。

最后貌似可以得出如下的结论:

像JavaScript与PHP这样的动态类型语言之所以流行的原因是因为它们"能跑起来"。你不需要学习经典静态类型语言里面所有复杂的语法(想想Ada和PL/SQL吧!)。你马上就可以开始写代码了。程序员知道哪些变量包含字符串,因此没有必要写下来。这没错,的确是没有必要什么都写出来。

看一下Scala(或者C#,Ceylon,以及几乎任意的现代编程语言):

val value = "abc"

它除了能是字符串,还能是什么吗?

val list = List("abc", "xyz")

这个除了List[Stirng],还能是别的类型吗?

不过请注意了,如果你需要显式声明变量类型的话,也是可以的——总会有那么些个边缘的用例:

val list : List[url="abc", "xyz"]String] = List[String[/url]

不过绝大部分的语法都是“可选”的了,它们能由编译器来完成推导。

动态类型语言已死

讲的所有这些的结论就是,一旦语法的冗长及阻碍从静态类型语言中刨掉之后,那么使用动态类型语言就完全没有任何优势了。编译器已经相当快了,部署也很快速。只要使用了合适的工具,静态类型检查所带来的好处是巨大的。(不相信?请读下这篇文章)。

举个例子,SQL也是一门静态类型语言,它的使用障碍主要是语法造成的。没错,很多人都认为它是一门动态类型语言,因为他们是通过JDBC来访问SQL的,比方通过一些无类型的SQL语句的字符串拼接而成。如果你写过PL/SQL,Transact-SQL或者jOOQ写过嵌入式SQL的话,你绝对不会认为SQL是动态类型的,你马上就会感谢PL/SQL,Transact-SQL,以及你的Java编译器给你的SQL语句所做的类型检查了。

那么,让我们摒弃这个由我们一手创造出来的东西吧,因为我们实在是太懒了,不想在代码里声明这些类型。让敲代码变得更愉快吧!

如果你是Java语言的专家组成员,并碰巧看到了这篇文章,请你一定要把var和val关键字,以及流敏感类型添加到Java语言中。我保证一定会爱死你的!

英文原文链接