作者 | Lee Phillips
译者 | 刘志勇
策划 | 刘燕
本文最初发表于 Ars Technica 网站,经原作者 Lee Phillips 授权,InfoQ 中文站翻译并分享。
科学计算已经被 Fortran 统治,但为大规模数值工作而生的 Julia 横空出世了。
最近,我在网上遇到了许多高兴又激动的科学家。
一种新工具使这些科学家和数学家兴奋不已。这种新工具并非新粒子加速器或超级计算机。取而代之的是,这个令人兴奋的科学研究的新工具是……一种计算机语言。
你可能会问,为什么计算机语言会如此激动人心?当然,有些编程语言会比其他编程语言更好,这要取决于你的目的和优先级。有些编程语言运行速度更快,而有些编程语言则更快,更容易开发。有些编程语言拥有更大的生态系统,可以让你从库中借用经过实战检验的代码,并且可以让自己完成较少的工作。有些编程语言非常适合处理特定类型的问题,而另一些编程语言则擅长于通用性。
对于从事计算的科学家来说,编程语言、编译器和库的质量,始终是至关重要的。Fortran 对于那些用来模拟大气层或设计核武器的人来说是传统的首选工具(这种情况显然经常发生,尽管它现在有更多的竞争对手)。这种编程语言在市场上一直占据着主导地位,因为有了编译器,人们因此可以充分利用最大的超级计算机。由于 Python 在生态系统中的发展势头及其交互性和快速的开发周期,对于当前这批数据科学家来说,Python 很受欢迎。
六年前,我曾写过一篇文章《科学计算的未来:能否有任何编程语言可以超越上世纪 50 年代的庞然大物?》(Scientific computing’s future: Can any coding language top a 1950s behemoth?),叙述了有关 Fortran 在科学计算领域的杰出地位,并将其与其他几种编程语言进行了比较。我在那篇文章的结尾做了一个预测:在十年之内,一种叫做 Julia 的新语言有望成为科学家们在解决大规模数值问题时所使用的编程语言。然而,我的预测并不十分准确。
事实上,Julua 编程语言只花了一半时间就实现了我的预测。
1
今年的 JuliaCon 2020 大会够刺激
近几年来,通过与科学家们的交谈,Julia 编程语言确实在这个行业中掀起了一股新的热潮。但是,当我准备写下它的潜力时,我真的并不明白为什么这种编程语言会如此流行。
我的评估是基于 Julia 独特的便捷语法和不妥协的性能的结合。当时,虽然 Julua 还没有到达 1.0 版本,但已经有了很多令人兴奋的讨论。看起来,Julia 已经解决了“双语言问题”:这是 Python 程序员以及其他表达型、解释型语言的用户经常面临的难题。你使用 Python 编写了一个程序来解决一个问题,并享受它友好的语法和交互性。这个程序的测试版可以处理你的问题,但是如果你想把它扩展到更实际的应用中,速度就会很慢。这并不是你的错。Python 本身就很慢,对于某些类型的应用来说,这并不重要,但对于你的大型模拟来说却非常重要。通过运用各种技术来加速计算,但只实现了很小的成果,你最终会转向用 C 语言重写计算中最耗时的部分(最常见的)。现在速度已经很快了,但现在你还是需要维护两种语言的代码,这就是所谓的“双语言问题”。
虽然 Julia 对这个问题的解决方案吸引了科学家和其他人对这门编程语言的兴趣,但这并不是这个平台带来新发现的兴奋点的原因,而是由于别的原因。
当我撰写本文时,JuliaCon(一年一度的 Julia 大会)今年在线举行。正常情况下,计算机会议的日程都是与编程、编译器、算法、优化以及其他计算机科学相关的内容。虽然在今年的 Julia 会议上有很多这样的内容,但浏览大会标题却会给人留下一种错误的印象,仿佛你是在参加一个科学会议。有各种各样的演讲,从流体动力学到大脑成像,再到语言处理。虽然在很多领域都有着令人惊异的多样性,但观看演示文稿却给人一种社区的感觉,像是受到了自由软件运动的影响。
所有人的代码都托管在 GitHub 上。如果你有兴趣在你的研究中使用某人的算法,你可以阅读源代码,并且你还可以访问最新的开发版本。到了一定年龄的科学家就会知道,这与过去的计算研究有多么大的不同。在过去,代码可是很少从实验室走出来。
在 Julia 社区中,还有很多东西可以统一起来:Julia 的魔力在于它可以促进协作和代码重用。下面是一些来自 JuliaCon 2020 大会演讲者的赞誉:
León Alday,分子建模。
这就是 Julia 在解决这些问题如此强大的原因之一……这种集成使 Julia 比其他语言更具优势……我们能够在很短的时间内开发出这些解决方案……
George Datseris,沃森博士,科学助理。
Julia 确实是允许这样的项目存在的编程语言。
Mauro Werder, 冰川厚度。
使用 Julia 编程是一件乐事。
Valeri Vasquez,媒介传染病动力学。
Julia 编程语言……是一个特别灵活的工具……
Rafael Schouten,空间模拟。
Julia 编程语言是显而易见的选择。
Meghan Ferrall-Fairbanks,肿瘤动力学。
通过 Julia 编程语言,我可以利用来自不同学科的工具来推进癌症研究。
Vilim tih,成年斑马鱼脑动态监视。
这项工作非常适合用 Julia 编程语言来完成,因为它有很好的抽象特性,可以编写非常通用的代码。
Petr Krysl,偏微分方程。
有一种可以用来写任何东西的快速编程语言真是太好了。……但是,这些天来,真正令我印象深刻的是,Julia 编程语言在某种程度上提高了我的工作效率。Julia 使我们可以轻松地在适当的抽象层次进行思考。
George Datseris,音乐演奏分析。
为什么用 Julia 编程语言进行科学研究这么棒!
这些科学家都发现,Julia 增加了合作的机会,也比以往任何时候都更容易整合他人的工作,使他们能够编写可以被他人以不可预见的方式使用的代码。这些能力的关键在于 Julia 编程语言解决了一个不同的古老难题,这一次是计算机科学的难题,即表达式问题。
2
通过比喻解释表达式问题
“表达式问题”的概念是研究计算机语言设计时提出的。这是计算机科学领域的一部分,所以关于它的意义、含义以及围绕这一问题的各种不同方法的现有解释都是抽象的,并且依赖于专业术语。但我们可以做得更好。所有的设计问题都可以用烹饪的比喻来描述。
我们想要通过比喻的方式来描述的计算机科学术语是函数 / 程序、数据类型和库 / 模块 / 包。简而言之,函数或程序就是获取某些输入、对其进行操作,并产生某些输出的过程。数据类型是数字或其他信息的集合,这些数字或信息可能具有各种各样的结构,由函数对其进行操作。而库是函数的集合,以及它们所使用的数据类型的描述,这些组合起来执行一组相关任务。库的一个例子是一组用于绘制图形的函数。库中的各个函数可以用来绘制不同类型的图形,如饼图和直方图。例如,饼图的数据类型是元素对的列表,第一个元素是单词或短语,第二个元素是百分比。
对于那些花时间在厨房里按照菜谱做菜的人来说,这种比喻是相当直接和自然的。库或包变成了菜谱;例如,想象一本关于制作甜点或汤的书。这些函数或者程序既可以被认为是制作菜肴的完整菜谱,也可以被认为是技术或程序,比如如何炒菜。我们可以把它们想象成齿轮,因为它们是用来加工原材料的机器。数据类型就是本练习中的原始内容。
想象一下,我们的菜谱书是这样组织的:菜谱只能用特定的配料。例如,你可以查“如何炒”并找到炒洋葱或炒大虾的程序和步骤。由于所用的原料不同,这些程序都是不同的。如果菜谱像计算机语言那样工作的话,那么配料表就是菜谱书的一部分,实际上是包含在菜谱书中的。
如果我们想要修改菜谱书,添加一个新的菜谱,或者描述一种新的技术(例如,可以用搅拌机做的事情),那么添加新的菜谱很简单。只需写一个新食谱,列出你感兴趣的原料,包括可能属于其他现有菜谱的原料。
现有的食谱都无需修改。事实上,我们正在增加新的食谱并没有让旧的食谱变得过时。
但是如果我们想加入一种新的原料,怎么办?我们当然可以用它来创造出全新的菜谱。但是,如果菜谱书中以前的菜谱都没有提到鱼,那么如果我想把鱼加入到那些菜肴中,我们就必须回过头来修改它们。我们将不得不改变我们已经完成的工作。
但组织一本菜谱书的方法可不止一种。如果它是围绕原料而不是围绕烹饪方法来组织的呢?对于每一种原料,都会有一套与之相配套的技术或方法。让我们接着想象一下,可以用下面的图片来表示:
这里的方法本身并不存在,而是与所使用的原料相关联的。现在,添加新的原料很容易。如果我们的菜谱书的修订版需要一个鱼的菜谱,我们只需编写处理鱼的程序,并将这些信息与鱼打包成一个整洁的包就可以了。
我们可以在书中添加原料,而不必更改任何现有的菜谱,这些菜谱仍然完全可用。
但是,如果我想在菜谱书中加入一种新的技术呢?如果修订后的版本是为了向你展示你可以用搅拌机做些什么呢?
如果不改变我们已经完成的工作,就不能添加新技术,因为现在的方法是包装在原料里面的。为了添加一种新的方法,我们需要处理之前整洁的包,并修改我们已经编写过的方法。
这两种组织菜谱的方式类似于两种计算机语言。围绕过程组织的菜谱书就像是用函数式语言编写的,而围绕原料组织的菜谱书就像是面向对象语言编写的。在一种情况下,你不得不重写现有过程来添加新的成分;而在另一种情况下,不重写现有工作就无法添加新过程,这就是表达式问题。再回顾一下计算机语言设计中的术语:在函数式语言中,你可以添加新的函数,而不必接触现有的函数;但添加新的数据类型则意味着要重写现有的函数。对于面向对象语言,你可以随意添加新的数据类型,但如果你希望添加新的函数,则需要重新设计现有的对象。
表达式问题意味着,使用这两种传统范式中的任何一种,都存在障碍,无法将一个包(菜谱书)扩展到新领域:以新的方式组合现有包的障碍甚至更多。在重用和组合现有代码时,一个包能否进行扩展而不改变已有的内容是至关重要的。这使得库作者可以用一种通用方式编写代码,而不必为每个应用程序维护特定版本。它允许最终用户扩展现有的模块,而无需引入新的错误,这些错误在修改时将不可避免地出现;而且也不需要维护每个库的私有版本。
3
多分派,也可以通过比喻来解释
显然,如果有一种方法能够组织菜谱书,让它可以自由扩展以处理新的原料,以及通过向菜谱书中添加新的方法,而不改变已经编写好的内容,那么这将是一个巨大的优势。与其严格按照方法或成分来组织这本书,也许还有一种更通用、更灵活的方式。代表这种新方式的图片可能看起来如下图所示。
上图意在表明方法与原料之间的自由关联;一种方法不从属于另一种方法。用不同颜色重复出现的齿轮应该表明,我们通常会创建现有函数的变体,以对不同的成分集进行操作,而不是不相关的函数的随机集合。
例如,我们假定的可扩展的菜谱书可能包含炸鸡制作的过程。如果我们想要通过增加炸鱼程序来扩充这本菜谱书,就不需要从头开始。毕竟,我们的想法是扩充这本菜谱书,而不是撰写一本新的菜谱书。我们可以编写炸鱼程序,指导读者继续进行与炸鸡制作相同的操作,但要使用稍微高一点的温度,并在 10 分钟后出锅,而不是 30 分钟。
在这个思想实验中,还可以通过想象掌握(现在)三种组织菜谱书的方法,来确定每种情况下目录看起来可能是什么样的。“函数式”版本的目录看起来可能如下所示:
第一章 煎炸
* 鸡肉
* 鱼
第二章 蒸煮
* 鸡肉
* 甜菜
请注意,我们是如何通过添加新章节来添加新方法的,而添加新原料就意味着更改现有章节。
“面向对象”版本的部分目录看起来可能如下所示:
第一章 鸡肉
* 煎炸
* 蒸煮
甜菜
* 蒸煮
* 炒
现在,我们可以通过添加新的章节来添加新的成分,但添加新的成分意味着要去修改已经完成的章节。
我们的第三种方法,也就是我们最大限度地可扩展的菜谱书,其目录可能如下图所示:
第一章 炸鸡
第二章 蒸鸡
第三章 煮甜菜
第四章 炒甜菜
显然,适用于任何成分或成分集的任何程序都可以作为新章节添加,而不必修改现有章节。你可以自由添加新的方法,也可以添加新的成分。
与前两个版本相比,这个最终版本可能看起来没有组织性。但在实际的元件库中,方法和成分之间的关系将是库结构的一部分。在我们的菜谱书比喻中,鸡肉和鱼肉可能都是肉的子集,草莓和樱桃可能被归类为红色水果的子集,煎炸和炒可能被视为更通用方法的变体,等等。
以这种方式组织一本菜谱书将是解决表达式问题的烹饪版本的一种方法,这是一个比喻,在语言设计中,被称为“多分派”(multiple dispatch),简单地说,就是指根据应用它的所有成分或数据类型的类型自动选择一个方法。
多分派是 Julia 解决表达式问题的方案。它也是语言的核心组织原则,因此,它既不是面向对象的,也不是函数式的。多分派比函数式或面向对象的解决方案更加强大、更加通用。多分派是一种秘诀,它使 Julia 变得更容易,并简化了大都数其他语言难以完成的任务:可以自由、直接地混合使用库并进行匹配,以及完成编写它们的人都无法想象的工作。
4
例如:傅里叶光谱
为了让这一点变得不那么抽象,我们可以来看一个例子,这个例子涉及到 Julia 事实上的标准绘图库 Plots.jl——绘图很容易:要制作正弦波的图形,一旦你将 x 定义为从 0 到 2π的点数组,那么只需键入 plot(sin.(x)) 就可以得到如下图所示的图形:
Sin 后面的点使三角函数“广播”到 x 的每个元素。
假设我正在创建用于傅里叶合成和分析的软件;例如,将声波分析作为谐波和。Julia 使你很容易创建自己的数据类型,而且关键的是,用户定义的类型不会减慢计算速度。我发明了一种叫做 Spect 的类型来包含波的谐波频谱。通常,Plot 绘制数字数组的图形,如上例所示。但是,只需几行代码就可以告诉绘图包,我想通过查看由频谱中的谐波和构成的波的一个周期的图像来可视化我的 Spect 数据类型。如果我创建一个包含方波前十次谐波的特定 Spect 实例,并将其命名为 sqw,那么我可以通过输入 plot(sqw) 来绘制这个图。
其他类型的图形将自动工作,因此我可以输入 scatter(sqw) 来查看。
一旦我有了光谱的数据类型,就可以编写函数对其进行操作,以各种方式转换光谱。我写了一个我称之为低通滤波器的东西,顾名思义,它能过滤掉超过指定值的谐波。为了过滤方波并将其绘制在一个步骤中,我可以说 plot(lowpass(sqw, 3)),它将给我提供一个三次谐波滤除之后的方波图。
我希望这个简短的可视化示例能够传达出自定义数据类型的一些强大功能,以及使用现有代码处理它们的能力。在这种情况下,绘图软件包并没有光谱的相关知识,但它却可以很容易地将其扩展为可视化。
Chris Rackauckas 博士是麻省理工学院的应用数学家,也是许多广泛使用的 Julia 软件包的开发者。他给我描述了其他类似的例子。例如,有一个用于测量的 Julia 包,或其他带有不确定性的数字。然后是解决微分方程的软件包。你可以将它们组合起来以生成包括不确定性在内的解决方案,当你要求绘制方程式的图形时,你就会得到其解的图形,其中有显示不确定性的误差条状图,从而组成了三个库,这三个库并不是为了相互配合而编写的。
Rackauckas 还有更多的例子:有一个叫做四元数的奇异数的 Julia 库,四元数有四个组成部分(就像我们熟悉的复数的更强版本)。你可以将这些数字与测量包结合使用微分方程求解器,当你绘制其解时,你会得到一个带有 4 条线的图,每条线都附有误差线。
5
工具的重要性
Julia 既不是第一个解决表达式问题的编程语言,甚至也不是第一个通过多分派解决这个问题的编程语言。Common Lisp 拥有这些特性都快 40 年了。其他一些语言,如 Perl 的最新版本,也拥有这些特性。这些语言的用户都写过关于它是如何大大增加编写和扩展库的容易性。不同之处在于 Julia 是围绕多分派设计的,而在其他编程语言中,多分派是可选的,而且会带来性能损失。多分派设计的目的是使语言具有灵活性,并能够自然地表达数学思想;但即使是这样,它的设计者对于社区中产生的大量代码重用也感到吃惊。
显然,对于我前文描述的那种流畅的可组合性,多分派或解决表达式问题的其他方式是必要的,但这还不够。Julia 在科学界得到了爆炸性的认可,因为它将这一特性和其他几个特性结合起来,对数学家来说非常有吸引力。它能够迅速地实现不费多少时间就能立即使用的代码。斯坦福大学的 Mykel Kochenderfer 教授使用 Julia 设计了一个国际标准的飞机防撞系统,他告诉我,Julia“具有高级能力,可以由人类解释,但它的运行速度和我高度优化过的 C++ 代码一样快。”(此外,尽管多分派设计和生成代码的速度通常作为语言的单独属性加以讨论,但一些速度实际上是 Julia 的函数分派策略的影响。)
Julia 有一种表达能力强但易于阅读的语法,尤其是处理数组方面。它为数值算法的并行处理提供了一条平坦的道路。其优势在于 Unicode 时代的设计。这与其他一些语法特性一起,使得数学看起来比其他任何编程语言更像数学。下面是 Julua 程序中的一行实际代码,使用了专门为 Julia 设计的字体。
Julia 的所有这些特性在早期就吸引了许多科学家使用这种编程语言,甚至在它的多分派范式的独特优势引起人们的关注之前,就已经吸引了一大批用户。
我从中得到的教训是,工具很重要。一个画家所能想象的绘画,是受他所知的画笔和颜料所能做的限制的;一个作曲家在脑海中听到的交响乐必须符合舞台上布置的乐器的特点和范围以及表演者的技巧。
Julia 为计算科学家提供的这种独特的优点结合,扩展了普通人在有限时间内所能够完成的工作。它使科学家能够想象没有它可能无法想象的事物。
作者介绍:
Lee Phillips,物理学家,并多次为 Ars Technica 做出贡献。曾写过关于 Fortran 编程语言的遗产、大气湍流、 以及 Emmy Noether 如何改变物理学等文章。
参考阅读:
https://arstechnica.com/science/2020/10/the-unreasonable-effectiveness-of-the-julia-programming-language/
InfoQ 写作平台欢迎所有热爱技术、热爱创作、热爱分享的内容创作者入驻!
还有更多超值活动等你来!