Crystal 的未来
(这篇文章是 Crystal 2015 年降临日历 的一部分)
这个圣诞夜,发生了一件奇怪的事情:我们正开心地用 Crystal 编程,当我们把目光从屏幕上移开的那一刻,一个半透明的影像出现在我们附近。这个实体走近我们,说道:“我是圣诞过去之灵。跟我来吧。”
我们看到自己在编写一种新的语言,这种语言将类似于 Ruby,但会被编译并且是类型安全的。在那时,这门语言真的就像 Ruby 一样:要创建一个空的数组,你会写 []
,或者 Set.new
来创建一个空集合。我们很高兴,直到我们意识到编译时间是巨大的,呈指数级增长,难以忍受,于是我们就感到悲伤了。
我们花了很多时间试图让它工作,但毫无进展。最后,我们决定做出改变:指定空泛型类型的类型,例如 [] of Int32
或 Set(Int32).new
。编译时间恢复正常。我们又有点高兴,但同时也感到我们正在失去一些 Ruby 的感觉。这门语言发生了分化。
我们回头看看圣诞过去之灵,想问问他这一切意味着什么,但我们发现它旁边出现了一个相似但不同的身影。她说:“我是圣诞现在之灵。加入我吧。”
在我们周围,一个小型但充满活力的社区正在用 Crystal 编程。他们很高兴。没有人提到必须为泛型类型指定类型的烦恼。每个人都感觉到 Ruby 的精神仍然存在:在熟悉的 API 和类中,在语法中,在强大的代码块中。此外,性能的提升(无论是 CPU 还是并发性),再加上更好的类型安全,确实得到了回报,因此,现在必须偶尔指定一个类型并不会让人感到麻烦。
我们再次看着鬼魂,想问问它这意味着什么:看来我们在过去做出了一个正确的决定,对吗?但是,就像之前一样,它旁边出现了另一个东西,一个机械的晶体形象。它说道:“我是圣诞未来之灵。跟我来吧。”
这个小型社区仍在用 Crystal 编程,尽管大多数人似乎不像以前那样高兴了。我们试图询问他们为什么,但没有人注意到我们的存在。我们试图使用一台电脑并在互联网上搜索 Crystal,但我们的手无法触碰任何东西。我们带着询问的目光转向鬼魂,注意到它的胸前有一个键盘和一个小屏幕。我们搜索了“Crystal 糟糕”,希望显示出关于这门语言的抱怨帖子。确实有不少。大多数都是关于编译时间和内存使用量巨大的。我们心想,“编译时间很长?”,“我们几年前就解决了这个问题!”,我们冲着鬼魂喊。我们从它那里得到的唯一回复是“正在编译……”,景象消失了,我们又回到了办公室,孤身一人。
回到现在
“我们做点数学吧”,我们说道。我们现在在 Crystal 中最大的程序是编译器,它大约有 40K 行代码。编译它大约需要 10 秒,并且需要 940MB 的内存来完成。我们其中一个 Rails 应用程序(包括其 gem 中的代码行总数和“app”目录中的代码)大约有 320K 行代码,是编译器的 8 倍。如果我们用 Crystal 重写它,或者至少做一个具有类似功能的应用程序,每次编译它都需要 80 秒,并且需要 8GB 的内存。这在每次更改后需要等待很长时间,而且内存消耗也相当惊人。
我们能用当前的语言改进这种情况吗?我们可以引入增量编译吗?我们花了一些时间思考如何缓存之前编译的语义结果(推断的类型)并将其用于下一次编译。我们观察到,一个方法的类型完全取决于参数的类型、它调用的方法的类型以及实例、类和全局变量的类型。
因此,一个想法是缓存程序中所有类型的推断实例变量类型,以及方法实例化的类型及其依赖关系(该方法依赖于哪些类型,以及它具体调用了哪些其他方法)。如果实例变量类型保持不变,方法代码没有更改,并且依赖关系(调用的方法)没有更改,我们可以安全地重复使用之前编译的结果(类型和生成的代码)。
请注意,上面的“如果”以“如果实例变量类型保持不变”开头。但我们如何知道呢?问题是,编译器通过遍历程序、实例化方法和检查分配给它们的变量来确定它们的类型。因此,我们实际上无法重复使用缓存,因为在我们输入整个程序之前,我们无法知道最终的类型!这是一个鸡生蛋的问题。
解决方案似乎是必须指定实例、类和全局变量的类型。有了它,一旦我们输入了一个方法,它的类型就永远不会改变(因为方法的所有非局部变量,如实例变量,都无法再更改)。我们可以缓存该信息并在后续编译中重复使用它。不仅如此,即使没有缓存,类型推断也变得更加简单和快捷。
这是正确的做法吗?我们将再次与 Ruby 稍微分道扬镳。我们想要什么样的未来?我们是想要坚持当前的方法,代价是每次编译之间必须等待很长时间?还是应该指定更多类型,但拥有更敏捷的开发周期?
我们真正想要的是一种使用起来有趣且高效的语言。每次都要等待很长时间才能完成编译,一点也不好玩,甚至比偶尔添加几个类型注释还要没意思。而且这些类型仅仅用于泛型、实例、类和全局变量:局部变量和方法参数不需要类型注释。考虑到这些类型变化的频率,与编写新方法和编译程序的频率相比,这感觉是一个值得改变的事情。
我们已经开始着手开发这个新的编译器,因为我们希望尽快完成,因为很多代码将会失效。虽然当前的编译器直接作用于 AST,但在新的编译器中,我们使用的是 流程图,这将使我们能够拥有一个更简单的编译器(任何人都可以理解并直接参与其中并做出贡献),以及更易于理解和优化的代码。它还将使我们能够引入新的特性,例如 Ruby 的 retry
,并且工作量很小,因为流程图允许循环和“goto”式跳转。
如果您想了解更多关于这种变化的信息,可以查看 跟踪问题。
问答
- 您什么时候会完成新的编译器? 我们还不知道。我们正在慢慢但稳定地开发它,以可读性、可扩展性和效率为目标,并首先关注最难的部分。现在我们已经了解了该语言支持的大多数特性,因此开发起来更容易。请记住,当前的编译器最初只是一个实验,并且是使用 Ruby 编写的编译器的移植版本,因此它的代码并不是最好的 Crystal 代码。
- 您还会继续开发当前的编译器吗? 既是,也不是。我们会修复容易修复的错误,并且会继续扩展和改进标准库。
- 我的所有代码都会停止编译吗? 很可能。但是,您可以使用当前编译器的
tool hierarchy
来询问它实例变量的类型,从而使升级更容易。事实上,我们可能会包含一个工具来自动进行升级,操作就是这么简单。 - 新的编译器会包含其他特性吗? 我们希望如此!通过这次改变,我们还计划使用常用的
&block
语法支持转发代码块。目前这是可能的,但它总是会创建一个闭包,但可以做得更好。我们还计划允许使用代码块进行递归调用,这在 Ruby 中可以做到,但在 Crystal 中却无法实现。我们还希望能够拥有Array(Object)
或Array(T)
,其中T
可以是任何类型的,这在当前版本的语言中也无法实现。因此,这些新的类型注释将为这门语言带来更多功能,作为补偿。 - 将来还会有像这样的重大变更吗? 我们非常确定答案是否定的。如果我们知道实例、类和全局变量的类型,那么给定一个方法,我们可以通过仅分析该方法及其调用的方法来推断其类型,因为我们知道
self
的类型和其参数的类型。目前,这是不可能的,因为某些方法的类型取决于您如何使用一个类(您分配给它的内容)。因此,这次改变将是最后一次重大变更。