跳至内容
GitHub 代码库 论坛 RSS 新闻提要

另一种语言

Ary Borenzweig

Crystal 具有全局类型推断。除了少数需要您显式声明的类型(例如泛型类型参数)之外,您可以在没有类型注释的情况下进行编程。

这是一把双刃剑。

一方面,不必显式声明类型确实很不错。这允许进行非常快速的原型设计,类似于动态语言。您可以快速勾勒出一个想法并不断改进它,而无需不断地重新输入类型。这在重构和重组代码时也非常有用,因为摩擦力非常低。例如,当您从一段代码中提取一个方法时,您只需指定参数的名称,编译器就会负责推断参数的类型和返回值类型。或者,您可以通过将某个值赋给实例变量立即开始使用它,而无需先声明它可能包含的类型。

但是,这种方法也有一些缺点。让我们分析一下每个缺点。

代码变得更难理解和跟踪

有些人说,如果没有类型注释,代码就变得更难跟踪。让我们看一个例子。

def sum(values)
  count = 0
  values.each do |value|
    count += value
  end
  count
end

values 的类型是什么?如何才能在不知道它操作的类型的情况下理解这段代码?

当您学习编程时,我们相信您一定会在某个时候接触到伪代码。它看起来与您在现实生活中看到的代码类似,只是它被简化了:您很少在那里看到类型注释。在给定的上下文中,类型是显而易见的。如果您添加类型注释和其他信息,它将使代码的意图更难理解。听起来熟悉吗?

在我们看来,Ruby 代码非常接近伪代码。在上面的代码中,没有类型注释。该算法很清楚:遍历 values 中的每个项,并将它们添加到 count 变量中。就是这样。values 的类型是什么?这并不重要。我们只关心它是否可以迭代(使用 each)以及是否可以求和。此外,名称 sumvaluescount 有助于理解方法的意图和变量的可能类型。

将此与您必须添加一些类型的其他语言进行比较

interface Iterable<T>
  def each(&block : T ->)
end

interface Addable<T>
  def +(other : T)
end

def sum(values : Iterable<T>) where T : Addable<T>
  count = 0
  values.each do |value|
    count += value
  end
  count
end

在这里,我们告诉编译器,存在一个类型 Iterable,它具有一个 each 方法,该方法会产生泛型类型 T 的元素。然后,我们还告诉编译器,存在一个类型 Addable<T>,它具有一个 + 方法,该方法使用与其自身类型相同的类型的数值进行运算。最后,我们将 sum 方法定义为对属于 Iterable<T> 类型的数值进行运算,其中每个 T 都实现了 Addable<T>

对我们来说,最后这段代码距离我们最初想到的伪代码更远了。此外,还有更多代码需要阅读和理解。

对此的一个可能的反驳是,现在更容易导航代码。我想知道 each 是如何实现的,或者 Addable 是什么,我知道在哪里可以找到这些信息。如果我们没有类型注释,我们就无法对动态语言做到这一点。

但是……等等!Crystal 不是动态语言。当它完成编译时,它会为使用的每个变量和方法分配一个类型。从这些信息中,我们可以重新创建代码并使其可浏览。事实上,Crystal 有一个(非常实验性的)工具可以做到这一点,如果您使用 crystal browser file.cr 编译您的代码,它将打开一个网页浏览器,您可以在其中查看变量的类型并导航方法。因此,在我们看来,这并不是一个有效的理由。

最后一点也意味着 Crystal 知道类的实例变量的类型。在 Ruby 中,您可能会看到一个带有 @foo 实例变量的类,并想知道它的类型是什么。别担心,运行 crystal hierarchy file.cr,您将知道确切的类型。如果您在规范文件中运行它,这将非常有用,因为它们显示了类的使用情况。将来,可以以类似于 RDoc 的文档格式查看此信息。

增量编译不可行

由于没有类型注释,编译器需要每次都从头开始推断所有内容的类型。没有办法将模块编译为目标文件或其他格式,然后在以后重用这些信息,因为乍一看没有任何(类型)信息。

幸运的是,Crystal 的编译器速度很快:编译整个编译器需要 5 到 10 秒(其中只有 2.3 秒用于类型推断阶段)。对于更大的程序,时间会更长,因此我们必须找到解决此问题的方案。

另一个问题是,很难做到 REPL。如果代码始终具有类型注释,我们就可以生成机器代码,而不用担心变量的类型可能会更改,甚至类型实例变量的创建或更改。

很多时候,我们都想要放弃。“如果我们要求程序员在实例变量和方法中添加类型注释,增量编译和 REPL 可能会成为现实”。“至少语法会很愉快”。幸运的是,每当我们中的一人这样说时,另一个人都会用一个大大的“不”来回答。

这个“不”有充分的理由。如果我们朝着这个方向改变语言,最终我们会得到另一种语言。Crystal 是一种您可以省略类型注释的语言(请注意:如果您确实想添加,您可以添加类型注释)。但是,如果您被迫添加它们,那么它将不再是 Crystal。它可能与现有的编程语言之一非常相似。我们为什么要这样做?发明另一种类似于现有语言(或正在开发的语言)的语言有什么好处?没有。这将是浪费时间,是重复劳动。

确实,如果我们不放弃,我们将面临更艰巨的挑战。增量编译真的不可能吗?我们能不能想出一个类似的技术?在这种语言中,REPL 是否可以通过某种方式实现?出现了一些非常有趣的问题。出现了具有挑战性的问题。快乐的时光来了。

我们相信,可以设计一种语言,在这种语言中,编译器不需要类型注释,但类型仍然存在。我们想要一个智能编译器。我们不想为了使我们的工作(编译器编写者)更容易而简化语言。我们希望使程序员的工作更容易。而且有趣 :-)