函数式程序设计语言YATTA的设计

2020-05-26 23:06:40

这就是雅塔语的故事。YATTA是一种面向GraalVM的函数式、动态、非阻塞语言。今天是它首次公开发布alpha的日子。

我相信许多读者会问的第一个问题是“为什么还要使用另一种编程语言?”冒着听起来傲慢的风险,我的答案很简单:我想要一种像Yatta这样的语言,一种允许我将程序编写为简单表达式的语言,而不必担心诸如并发性之类的低级内容,具有漂亮的语法和强大但动态的类型系统。我不想与任何低于这一点的人妥协。

这篇文章是以一种非正式的风格写的,重点是这个项目背后的历史和动机。它也是从我的角度编写的,但我想强调Fedor的贡献,特别是在运行时和数据结构领域,同样重要的是,对语言特性进行了无数小时的一般讨论。

让我跳到这里,给你一个我们想要在这个博客中达到的顶峰。希望在深入研究所有其他语言的问题之前引起您的注意。

尝试让KEYS_FILE=File::OPEN";TESTS/Keys.txt";{:READ}VALUES_FILE=File::OPEN";TESTS/Values.txt";{:READ}KEYS=File::READ_LINES KEYS_FILE VALUES=File::READ_LINES VALUES_FILE()=File::CLOSE KEYS_FILE()=File::CLOSE VALUES_FILE in Seq::ZIP KEYES VALUES|>;dict::from_se.。{}结束|>;打印。

不要担心没有理解所有的语法,这在目前并不重要。这里发生的事情是这样的:程序并行读取两个文件(一个包含键,另一个包含值),然后将键/值压缩成对创建一个字典。

可能不清楚为什么并行读取这些文件。这就是雅塔的全部意义所在!

如果您注意了,请继续往下读。稍后我会详细介绍,但首先我想分享一些更广泛的背景和我创建这种语言的动机。

从我开始学习函数式编程概念开始,我就发现它们非常有用。过去使用Scala和Erlang是一件令人愉快的事情,尽管它们的理念截然不同,但它们都有一些很好的特性,比如一流的函数、不变的数据结构或强大的模式匹配。在下面的文本中,我并不是要批评任何现有的语言,但我确实想指出一些我认为有用或困难的具体观点。这应该会提供足够的背景来解释为什么Yatta是这样设计的。

Scala是我学习并实际用于生产环境的第一种函数式编程语言。我以前主要是一名Java工程师,所以我觉得这是进入FP世界的一种自然方式。我对Scala的主要期望是能够轻松地并行化一些代码,这在当时的Java中是相当痛苦的(至少在语言级别上仍然是如此)。

请注意,在这一点上,我仍然对如何向代码添加并行性非常感兴趣。雅塔是专门设计的,我再也不用去想那件事了!

起初,我对Scala非常满意--能够重用我现有的大部分Java代码是非常棒的。能够慢慢地从OOP概念转向FP风格是件好事。在后来的某个时刻,我意识到我的大部分代码都是以函数风格编写的,没有任何继承,但是使用了模式匹配、不可变的案例类和更高阶的函数。

CASE类非常适合存储数据,不再需要为DTO而烦恼。在对并发性进行推理时,拥有不可变的类型是非常有益的。都是好事。然而,过了一段时间,我开始挣扎。我没有一台速度很慢的计算机,但随着我的项目的增长,编译时间也随之增加。增量编译有所帮助,但仅在一定程度上有所帮助。我使用了相当多的Scala库,有时我一天中的大部分时间都在与复杂的类型错误作斗争,缓慢的编译时间更是雪上加霜。

当然,编译器确实捕获了一些错误,但有时我真的想知道,捕获的这两个错误是否真的值得花费大量时间等待编译完成或与难以理解的类型错误作斗争。这里通常要归咎于构建在大型库和框架中的复杂抽象将编译器推向极限。

最终,我在Erlang完成了一个项目。由于之前没有使用它的经验,我不确定会发生什么。毕竟,Erlang是动态类型的。它也不支持Java等语言中的“传统”OOP概念。它有一个“奇怪的”语法和一个不同寻常的并发模型。有很多东西要学。

我很快就爱上了二郎。我最初最害怕的事情(比如缺乏OOP特性)原来是那些允许更简单的语法的东西。另外,它有一个内置的并发模型,而不是Java中基于锁/监视器的基本同步。然而,我最担心的是Erlang是一种动态语言。我一直以为我永远不会喜欢一门充满活力的语言。我的心态仍然非常倾向于静态类型,尽管我有过处理不清楚类型错误和编译时间缓慢的痛苦经历。

从那时起,我又使用了一些Java、Scala、JavaScript、TypeScript、Python,但我觉得它们都有一些不应该出现的东西:这些都不是纯函数语言,它们仍然实现了OOP特性。自从我第一次使用Erlang以来,我从来没有觉得需要OOP(至少在主流形式下,需要继承)。现在,每一种支持OOP的语言对我来说都是臃肿的。我不想再处理这件事了。我最终也改变了对静态输入的看法。我不想再次落入缓慢编译和调试神秘键入错误的陷阱。

我不必简单地回到Erlang的唯一原因实际上是它的并发模型。过了一段时间,在进程之间发送消息开始让人感觉有些低级。我开始怀疑这个抽象级别是否需要出现在大多数用户级代码中,或者是否可以隐藏在语言“本身”中。

大约两年前GraalVM出现时(或者至少当它引起我的注意时),我立即开始计划如何使用它来创建一种我想使用的编程语言,而不受现有语言的妥协和限制。这意味着首先要有一种函数式语言。充满活力。动态的,因为我觉得现在有这么多静态语言,似乎没有人真正知道哪种类型系统是“最好的”。每种静态语言都有一些不同的方法,但是哪种方法是“正确的”呢?哈斯克尔有几十个分机吗?Scala及其图灵完整类型系统?也许,我肯定不是在反对这些在其领域非常成功的语言。伊德里斯最终会不会做对了呢?

然而,我不想将自己锁定在特定类型系统的约束中,然后将其他所有决定都置于这些自我强加的限制之下。我希望拥有动态类型系统的灵活性,然后尽最大努力避免类型系统以外的不同抽象级别的错误。“当然,这是以运行时成本为代价的!”,您可能会这样想--这是真的。但这正是GraalVM的用武之地,它为Yatta提供了JIT和基于运行时配置文件优化代码的能力,因此希望实际成本不会那么糟糕。

简单明了的语法,最小的样板,很少的关键字,不需要OOP构造。代码必须非常容易阅读。

一流的函数和模块。模块只是函数和记录的集合。模块可以进一步组织到包中。支持尾部调用优化的函数调用。

数据类型很少。YATTA具有以下内置类型:整数(64位)、浮点数(64位)、字符(UTF-8)、符号、序列(可变长度,两端的恒定时间访问)、元组(固定长度)、字典、集合和STM(软件事务内存-测试版)。但是,类型之间没有隐式转换!

自定义数据类型或记录。记录基本上只是带有命名字段的元组,以及与之相伴的语法糖。

强大的模式匹配,允许嵌套匹配和非线性模式(可以在模式中多次使用相同的名称)。

非常简单的并发模型。即使它在某些(或大多数)情况下不是性能最好的,它也应该内置于语言中,并且非常容易使用。理想情况下只使用函数构建,不需要特殊的语法结构。通过STM实现高级并发。

在JVM(GraalVM)上运行,并允许与其他JVM语言进行多语言协作。如果与现有代码库集成,则非常重要。

当然,这组优先事项是非常个人化的,并且来自我以前使用其他语言的经验。其他人可能有不同的优先事项。不管是哪种方式,我很快就开始了这门语言的实现工作。在接下来的段落中,我将尝试更深入地研究它们中的每一个。

我知道我想要一个简单的语法,但这是什么意思呢?LISP语法绝对简约(从技术上讲),但不知何故非常难看。包括我在内的一些人认为Python的语法非常简单。毕竟,这是Python的主要目标之一。但是我想知道,它真的是可能的“最简单”的语法吗?Haskell的语法可以说是更简约的,但是再说一遍,它真的很简单吗?我不这样认为。Haskell允许由各种随机字符组成的自定义运算符(乍一看),我认为如果您不每天使用Haskell语言,Haskell程序是最难读的程序之一。因此,简单对我的真正意义是这样的:

关键字的最小值。到目前为止,它们只有20个左右。具体地说:let、in、if、Then、Else、true、false、module、exports、as、case、of、import、from、end、do、try、catch、raise、module、record。这份清单在未来可能会略有变化,但目标是明确的。吃得越少越好。

几种类型的表达式。没有任何声明。这减少了理解正在发生的事情所需的脑力。无需考虑Monad转换或具有X种方式来定义特定于类型的行为(类型类、继承、宏、隐含、…)。。说得够多了,Yatta有这些类型的表达式:case/if/let/do/try+catch/raise,然后是文字表达式,比如数字或字符串,或者函数和模块。到目前为止,这就是所有的内容,我希望将语言真正保持在尽可能小的范围内。

简单的风格是关键,少就是多,这意味着没有分号,没有花括号,甚至没有任何“块”。

流水线语法-便于链接函数调用。支持的运算符|>;和<;|将函数结果重定向到右侧或左侧。

生成器语法作为Transducers(归约函数的泛型转换)周围的语法糖,允许轻松迭代和构建数据结构,如序列、集合或字典。

考虑到这些要求,我相信Yatta的语法实际上非常小且易于阅读。(我建议您浏览一下该页面,我试图使其非常简洁而全面地描述所有语法和所有类型的表达式,包括示例)。

在这一点上,我终于可以回到我开始时的内容,并解释这一切是关于什么的。而我相信,这才是雅塔真正脱颖而出的要点。这并不是说在Yatta运行时中实现的想法在以前是闻所未闻的,但是它们在运行时级别上的实现使得该语言非常容易和直观地使用。

这里的关键点是:Yatta基本上消除了已经计算的值和承诺(或Java/Scala中的Futures)之间的任何差异。这意味着程序员不必编写任何不同的代码,这取决于值是已经存在还是将在稍后进行计算。

考虑到这一点,您希望在Scala中并行读取两个文件(一个包含键,另一个包含值),然后将键/值压缩成对创建一个字典。在Scala中执行此操作的典型方式如下所示:

当然,可能有一些库可以让它变得更短、更优雅,但是对于基本的Scala来说,这可能是可行的方法。现在在雅塔这样做怎么样:

尝试让KEYS_FILE=File::OPEN";TESTS/Keys.txt";{:READ}VALUES_FILE=File::OPEN";TESTS/Values.txt";{:READ}KEYS=File::READ_LINES KEYS_FILE VALUES=File::READ_LINES VALUES_FILE()=File::CLOSE KEYS_FILE()=File::CLOSE VALUES_FILE in Seq::ZIP KEYES VALUES|>;dict::from_se.。{}结束|>;打印

这两个例子都显示了完整的程序,包括所有的“样板”。正如你所看到的,在雅塔几乎没有这样的东西。更重要的是,Yatta中的代码本质上也是非阻塞的,并且两个文件都是并发读取的,而无需编写任何额外的字符来实现这一点。重要的是您想要对这些文件做什么,而不是如何实现它。

雅塔是怎么做到这一点的?有几件事:首先,它知道do和let表达式之间的区别。它们都用于评估多个计算步骤,但是确实要确保这些步骤按照定义的顺序进行,让我们尝试并行化非阻塞任务。

这里发生的事情是这样的:Yatta将首先对let表达式执行静态分析,以确定步骤之间的依赖关系。它知道在File::Read_Lines函数中使用了KEYS_FILE和VALUES_FILE,还知道键和值不相互依赖,因此它们可以并发运行。还要知道这一点,YATTA没有并行KEYS_FILE和VALUES_FILE,也没有并行关闭文件--尽管看起来应该并行化--这些行之间也没有依赖关系。诀窍在于File::Read_Lines是一个函数,它返回运行时级别的承诺(对用户隐藏),这意味着并行读取两个文件实际上是可以的,否则您将不得不阻塞它,那么为什么不同时阻塞正在读取的两个文件呢?

这里的另一个重要概念是,let表达式中定义的别名顺序确实很重要。Yatta不只是根据依赖关系随机地重新排列它们。如果是这样的话,例如,它可以在读取这些文件之前将其关闭。这是不正确的。Yatta只是使用对该表达式的静态分析来确定哪些别名可以“批处理”,如果它们提供底层承诺,则实际上会对执行进行批处理。然后将整个表达式转换为如下所示:

执行批次1(顺序执行,因为File::Open不返回承诺):KEYS_FILE=File::Open";Testing/Keys.txt";{:Read}Values_File=File::Open";Testing/Values.txt";{:read}并行执行批处理2(因为File::Read_Lines确实返回承诺):Key=File::Read_Lines KEYS_FILE VALUES=File::READ_LINES VALUES_fileResult from Batch 2是聚合了键和值的承诺,完成后将执行批处理3:()=File::Close Key_file()=File::Close Values_file最后,整个let表达式现在是承诺,因此只要准备就绪,就运行最终表达式:SEQ::ZIP KEYS。

Yatta会根据需要自动链接运行时承诺,更重要的是,每当它们完成时,它都会“展开”它们,因此运行时实际上不会因为到处传播承诺而变得臃肿。一旦计算出承诺,它就会再次变成正规值。

请注意,此处为返回承诺的表达式捕获异常与捕获正规值的异常没有什么不同。这是因为Yatta在所有情况下都对用户完全和透明地隐藏了两者之间的区别。

YATTA包含一些由Fedor构建的最先进的数据结构。这包括SEQUENCE、SET、DICTIONARY和STM。我认为这一节值得发表,因为Fedor在优化这些数据结构上付出了巨大的努力,它们不仅提供了出色的性能,而且还提供了一些有趣的特性,例如,UTF-8字符或字节序列将自动将它们编码在引擎盖下的块中,从而大大减少内存使用和垃圾收集器压力,同时仍然提供丰富而灵活的功能API!

在我看来,Yatta确实推动了动态语言的期望。它提供了一个抽象级别,使得编程只针对表达式。它消除了编程“如何”的需要,并将重点重新放在“做什么”上。尽管这听起来很常见,但对于所有的声明性语言,我相信Yatta在这方面比大多数语言走得更远。

这篇博客文章并不全面。这里甚至没有提到更多的功能,所以请务必阅读文档。

如果你很好奇,可以阅读Yatta主页,设置一个本地的GraalVM,然后安装Yatta来玩。安装非常简单-只需在设置GraalVM之后执行一个命令。

我建议,克隆项目repo,并从使用语言/测试程序开始。快跑吧:

这个alpha版本是第一个公开发布的版本,包含了大部分语法和语义。标准库仍然相当小,下一步工作的重点将是大幅扩展它。GitHub项目托管了问题跟踪器,有一个Google组可以对语言进行一般性讨论,还有一个Gitter房间可以与我、Fedor或任何感兴趣的用户进行快速聊天。

请随时建议下一篇博客帖子应该是关于什么的。我会继续撰写关于新功能或改进的文章,并保持版本说明的更新。

通过主演GitHub项目、订阅和共享此博客,或者贡献您的反馈、想法、您最喜欢的编辑器/IDE集成、库或任何您认为可能会有帮助的东西来表示支持!