原来的:https://elixir-lang.org/blog/2022/10/05/my-future-with-elixir-set-theoretic-types/

本文是关于 Elixir 和我的未来的系列文章,共三篇。ElixirConf 欧洲 2022什么时候ElixirConf 美国 2022包含来自主题演讲的节选

2022 年 5 月是 Elixir v0.5 的 10 周年,这是 Elixir 的第一个公开版本。

在这种情况下,您可能想预测 10 年后 Elixir 会是什么样子。但我认为这是徒劳的。因为 10 年前,Elixir适合网络开发但是也,嵌入式软件或进入诸如Nx(数值药剂)探险家轴突活本我从没想过我会通过以下项目进入机器学习和数据分析领域Elixir 被设计成可扩展的,社区一直在研究如何扩展它。

出于这些原因。我决定专注于 Elixir 和我的未来。这些是我个人对与其他社区成员合作感到兴奋的项目。今天这篇文章的主题是 2022 年 5 月 ElixirConf EU 演讲中讨论的类型系统。

我对(房间里的大象)视而不见的问题:类型

多年来,Elixir 核心团队一直服务于社区的最大需求。Elixir v1.6 引入了 Elixir 代码格式化程序。这是因为不断发展的社区和大型团队已经认识到大型代码库需要样式指南和约定。

在 Elixir v1.9 中,我们决定在语言中构建和支持发布功能:应用程序代码。发布包含其所有依赖项以及整个 Erlang VM 和运行时的自包含存档的能力。目标是通过将 Elixir 和 Erlang 社区都尝试过的方法引入官方工具来解决部署 Elixir 项目的困难。将发布功能作为语言特性纳入,为未来的自动化开辟了道路,例如mix phx.gen.release,它会自动生成针对 Phoenix 应用程序量身定制的 Dockerfile。

鉴于我与社区的关系,如果不提及静态类型,就谈论 Elixir 和我自己的未来是不诚实的,这似乎是当今社区最大的需求。但是,当社区要求 Elixir 使用静态类型时,我们实际上期待什么呢? Elixir 社区从静态类型中获得了什么好处?

类型和长生不老药

不同的编程语言和平台从类型中获得不同的价值。其他语言的价值观可能适用于也可能不适用于 Elixir。

例如,类型可以在其他语言中提供性能优势。但是,由于 Elixir 在动态类型的 Erlang VM 上运行,因此键入 Elixir 代码并不能提供任何有意义的性能提升。

类型的另一个优点是它们有助于文档(请注意,我不认为类型会取代文档,所以我敢于使用“辅助”这个词)。灵丹妙药已经有了类型规格享受同样的好处我们预计集成系统将为该领域带来更多价值。

然而,在代码维护的上下文中讨论静态类型的优点和缺点时,往往会被误解或夸大,尤其是在将类型与其他软件验证方法(例如测试)进行比较时,会增加。在这种情况下,诸如“静态类型系统将捕获 Elixir 代码中 80% 的错误”或“具有静态类型意味着编写更少的测试”之类的不切实际的陈述。我听到了很多这样的说法。

ElixirConf Europe 2022 主题演讲探讨了为什么我认为这些说法不正确但是,如果不讨论静态类型系统应该识别的确切类型的错误,那么静态类型系统有助于检测错误的说法是没有用的。

例如,Rust 的类型系统有助于防止诸如多次释放内存分配、悬空指针和线程中的数据竞争等错误。但是在 Elixir 中添加这样的类型系统会适得其反。垃圾收集器和 Erlang 运行时已经保证了阻止它们的属性,因此这不是您首先会遇到的错误。

这带来了另一个问题:类型系统。要证明代码的某些属性,您必须拒绝某些编程风格,这自然会限制您可以编写的代码量。但是,我们不想限制 Elixir 的表达能力。因为,老实说,语言语义)——主要是从 Erlang 继承的——我对此很满意。

对于 Elixir 来说,类型系统的好处主要是合约编程,指的是调用者 (caller) 和被调用者 (callee) 必须满足的条件才能保持一致,称为合约。维基百科上的合同设计或者,Bertrand Meyer 的面向对象软件构建(日文翻译:面向对象原理/概念介绍面向对象方法论/实践介绍)请参阅。然而,后者是一个巨大的体积)。如果函数caller(arg) 调用一个名为callee(arg) 的函数,这两个函数都会随时间变化,因此调用者(caller)将有效参数(arg)传递给被调用者(callee)并希望确保调用者(caller)正确处理来自被调用者(callee)的返回类型。我们要确保@和返回类型一致)。

确保提供此类功能似乎过于简单。然而,即使是小的代码示例有时也很困难。例如,假设您定义了一个返回负数的negate 函数。你可以像这样实现它:

def negate(x) when is_integer(x), do: -x

我们可以说negate 的类型是integer() -> integer()

在这里定义了negate,我们可以定义subtract函数如下:

def subtract(a, b) when is_integer(a) and is_integer(b) do
  a + negate(b)
end

因为我们只处理整数,所以这一切都按预期工作和类型检查。但是想象一下将来有人决定让negate 多态。因此,假设negate 函数也适用于布尔值:

def negate(x) when is_integer(x), do: -x
def negate(x) when is_boolean(x), do: not x

如果negate 的类型是integer() | boolean() -> integer() | boolean(),那么subtract 的实现你会得到一个像这样的误报警告:

Type warning:

  |
  |  def subtract(a, b) when is_integer(a) and is_integer(b) do
  |    a + negate(b)
         ^ the operator + expects integer(), integer() as arguments,
           but the second argument can be integer() | boolean()

(运算符+ 需要两个整数参数,但第二个参数是整数或布尔值)

因此,我们需要一个类型系统,允许我们在函数之间进行类型约定编程,同时避免误报等误报警告,并且不限制 Elixir 语言。平衡这些权衡不仅是技术挑战,也是对社区需求的考虑。在 Erlang 中实现并在 Elixir 项目中可用透析器项目选择避免误报。但是,这意味着可能无法检测到某些错误。

在这一点上,整个社区似乎更喜欢检测更多潜在错误的系统,即使这意味着更多误报。这在 Elixir 和 Erlang 的上下文中尤其棘手。因为我自信的语言(注:明确说明程序假定某个条件始终成立,意思是如果情况不成立,就意味着它陷入了一个意想不到的状态。在 Elixir 中,当检测到这种情况时,它是推荐 crash = 产生错误并异常终止进程。这是为了检测子进程的异常终止并恢复/重启。Elixir 的优势之一是通过将其与调用主管的能力相结合,可以处理错误比try ... catch ... 更合适,这是用其他语言完成的。这是因为我喜欢将其解释为“带有断言的语言”)。编写在遇到意外情况时崩溃的代码。这是因为它依赖于主管来重新启动应用程序的某些部分。这个工具是用 Elixir 和 Erlang 语言构建自我修复、容错系统的基础。

另一方面,这就是 Erlang/Elixir 的类型系统如此令人兴奋和独特的原因:编译时和运行时故障模式。它能够优雅地处理导致功能单元丧失能力的原因的类型化执行一项功能。毕竟,无论选择何种类型的系统,都会遇到意想不到的场景,尤其是在与文件系统、API、分布式节点等外部资源交互时。

大公告

这导致 ElixirConf EU 2022 发布了一个重大公告:我们有一个持续的博士奖学金来研究和开发基于集合论类型的 Elixir 类型系统。Guillaume Duboc(博士生),在José Valim(本文的原作者和Elixir)的支持下,在Giuseppe Castagna(高级研究员)的指导下,开展了这项博士奖学金。

该奖学金是国家科学研究中心什么时候偏僻的是合作伙伴这是超级基地(招聘),芙蕾莎(招聘),达世币由...赞助这些公司都对 Elixir 的未来进行了大量投资。

为什么是集合论?

我们想要一个可以优雅地模拟 Elixir 的所有习语的类型系统。乍一看,我认为集合论类型完全一致。集合论类型使用集合运算来定义类型,以使它们满足相应集合运算的关联和分配属性。

例如,Elixir 中的数字可以是整数或浮点数,因此您可以将它们写为并集integer() | float()(相当于float() | integer())。

回想一下之前的 negate 函数:

def negate(x) when is_integer(x), do: -x
def negate(x) when is_boolean(x), do: not x

使该函数成为具有(integer() -> integer())(boolean() -> boolean()) 两种类型的函数,可以认为是这是共同的部分。这自然解决了上一节中描述的问题。如果用整数调用,它只能返回整数。

Elixir 还有一个称为原子的数据结构。它们唯一地代表由它自己的名字给出的值。例如,:sunday:bananaatom() 类型可以被认为是所有原子的集合。此外,:sunday:banana这两个值都包含在所有原子的集合中,因此可以认为它们是atom()的子类型。 :sunday:banana 也称为单例类型,因为它们只有一个值。

实际上,每个整数也可以看作是属于integer() 集合的单例类型。在类型系统中将值选择为单例在很大程度上取决于上一节中定义的权衡。

此外,类型系统应该是增量的,因为类型化的 Elixir 代码需要与非类型化的 Elixir 代码组合和交互。

就个人而言,我认为集合论类型是一种优雅且易于理解的类型推理方法。毕竟,Elixir 开发人员在创建具有多个子句的函数时不必考虑公共部分,而是直接建模)。

虽然 Elixir 的语义和集合论类型似乎以这种方式兼容,但将两者结合起来存在未解决的问题和已知的挑战。这里有些例子:

  • Elixir 对模式匹配和保护中使用的习语有强大的表示,但它们都可以映射到集合论类型吗?
  • Elixir 的关联数据结构,称为 Map,既可以用作记录,也可以用作字典,但是它们可以在统一的基础架构中输入吗?
  • 渐进式类型系统需要引入运行时类型检查以保持完整性,但由于这些类型检查是 Erlang VM 已经执行的类型检查的补充,这可能会降低性能。那么是否有可能利用 Erlang VM 执行的现有运行时检查并使生成的类型系统保持健全?

正是这些挑战让我兴奋地与 Giuseppe Castagna 和 Guillaume Duboc 合作。我认为在深入研究实施之前将这些问题及其解决方案正式化很重要。要从集合论类型开始,请参阅使用并集、交集和否定类型进行编程被推荐。

最后,需要注意的是,我们目前不打算处理某些领域,例如在进程之间输入消息。

期望和路线图

在这一点上,你可能会期望 Elixir 在未来的某个时候肯定会成为一种渐进类型的语言。但我们还有很长的路要走,所以重要的是要注意,情况可能并非如此。

实现类型系统的挑战之一是它感觉像是一个不可分割的步骤,至少对于像我这样没有相关教育的人来说。一是在此过程中没有很多见解或机会获得反馈。因此,我们计划在 Elixir 中逐步加入一个类型系统,我称之为“渐进式类型系统”:progressive(逐渐)向语言中添加渐进式类型系统来添加。

我们目前正在进行的第一步是利用 Elixir 程序中现有的类型信息。如前所述,在 Elixir 中您编写断言代码。也就是说,模式和守卫有很多类型信息。我想获取这些信息并将其用于现有代码库中的类型检查。 Erlang 编译器已经这样做以提高单个模块的性能,但最终我们希望跨模块和应用程序也这样做。

在这个阶段,Elixir 开发人员无需更改一行代码即可利用类型系统。当然,它只捕获了一些现有的错误,但这使我们能够收集压力测试、基准测试、开发人员反馈,并在幕后进行改进(或者如我们预期的那样,如果您认为出现问题,您甚至可以撤消所有操作) .

下一步是将类型化结构引入到语言中。当在整个代码库中进行模式匹配结构时,这允许结构类型在系统范围内传播。这个阶段引入了一个用于定义结构的新 API,我们还没有介绍过。开发人员应该使用新的 API 并从中受益。

最后,一旦我们对收集到的改进和反馈感到满意,我们将引入一种用于在 Elixir 代码库中输入函数签名的新语法。您可以迁移到这包括支持更高级的功能,例如多态类型。这些允许您输入复杂的结构,例如在Enum 模块中找到的结构。

要记住的重要一点是,这些功能将逐步探索和开发,并有大量机会从社区收集反馈。我们也希望我们的经验能够帮助其他希望将类型系统逐步引入现有编程语言的生态系统。

感谢您的阅读。另外,在“Elixir 和我的未来”系列的未来文章中再见。


原创声明:本文系作者授权爱码网发表,未经许可,不得转载;

原文地址:https://www.likecs.com/show-308628333.html

相关文章: