Beth Kjos

撼天动地的项目构想、简历,以及两者之间的一切。

TL;DR: Effect systems 是当前研究的热点领域。在快速研究了当前的主要思路后,我认为它们未能给软件开发的_专业_带来重大的实际好处。相反,我对此感到担忧。(进一步的讨论表明,好的解决方案正在开发中。)

Algebraic Effects:_又一个_走向完美的错误?

Effect systems 最近非常流行。自然地,我四处看了看,想了解一下这个话题。老实说,我认为 Ivory Tower 正在犯一个严重的错误。

我打算说明 effects 与 exceptions 共享一个共同的_原罪_。这个罪就是动态作用域的应用。

更新: 一些评论员说,现在有一些 effectful languages 使用词法/静态作用域来实现 effects。在这些语言中,可恢复的 effects 实际上是_过程类型的参数_,可以是隐式的,也可以是显式的。因此,对于这些语言来说,_动态作用域_的反对意见不成立。

另一方面: 我们早就知道如何进行静态作用域的过程参数传递。要传递一组相关的过程参数,它们之间有一些共享的上下文,这开始看起来很像普通的面向对象依赖注入。

简要离题解释一下为什么这么火

想想 exceptions。你懂的:像 Java、C++、Python、Ruby 或者你其他喜欢的传统语言中的 try/catch/finally。

现在想想_可恢复的_ exceptions。catch 子句可以将引发 exception 的代码返回,并且可能带有一个参数。也许处理程序从数据库中获得了一条记录,或者使用了 GUI,或者直接从我的身体里取出来的。这都无关紧要。子程序恢复执行,对它的答案感到满意。

它是如何工作的?你可以在调用堆栈中安装 handlers,使用动态作用域和专门构建的语法。

现在,你还记得 Java 中的 checked exceptions 吗?是的,每个人都喜欢讨厌 checked exceptions。创造者的心是好的,但执行结果并不理想。Checked exceptions 具有传染性。所以 effect systems 的设计者说:“别担心;计算机会_推断_ throws 子句。”

哦,而且你必须在_某个_层面上处理所有事情,否则编译器会生气。

我有什么资格说这些?

我不是一个以编程语言研究为职业的人。相反,我是一个肮脏的、所谓的“软件工程师”,白头发比我愿意承认的要多。Dijkstra 说过,当一个人不能做的时候应该如何编程?是的。所有这些。我供认不讳。但你要打你手中的牌。我很高兴与一些非常聪明的人一起工作。他们中的许多人比我更有学术背景。而且,他们中大约有一半人没有 CS 背景。我们有物理学家、经济学家和地球化学工程师。我们有一辆小巴,把老太太从_养老院_接来,教我们这些年轻人维护那些老太太在 Hula Hoop 流行之前编写的 COBOL 系统。

The Company 认为这一切都是一件好事。毕竟,甜蜜的职业生涯就是这样炼成的,所以我有什么理由反对呢?编译世界;Java Python C。每个人都在寻找一些 bug。有些人想维护你。有些人想被维护。

我最好停下来,免得模仿警察试图提取版税。

现在回到正题

我刚才说到哪儿了?哦,对了。Effect types. 没错。

因此,看起来,作为一种 effect language 的用户,我可以将控制权(和数据)传递给一个环境处理程序,并且可能在某个时候得到一个答案,这取决于所讨论的处理程序的语义。这有点像 Python 中的 yield 关键字,只是有一个 整个动态作用域的森林 的东西可能会介入,从调用堆栈(或代码库)的半路抢夺控制权。

工业代码库会增长到巨大的规模,并且持续很长时间。50 年前的代码今天仍在银行运行。维护人员来来去去,传递火炬——但不是他们积累的深厚知识——给他们的继任者,而且间隔时间不固定。

Effect systems 故意省略了原因和结果之间的过程间关系。这掩盖了维护人员为了能够有效地_并且在本地_推理一段给定的代码所需要的一些最重要的信息。我可能很幸运地知道_发生_了某件事,但不知道_何时、何地或为什么_发生。但是,合理的 OO 架构将所有这些基本的信息元素直接表示在代码中。(事实上,你可以通过这条原则来概括“合理的 OO 架构”的含义。)

工业界的传统语言都有一种模式,如果我想打开一个文件并写入,我就打开一个文件并写入。或者其他什么。文件、时钟、数据库、网络连接。一切都在那里,在全局范围内,随时随地都可以使用。语言定义中没有任何东西将_责任_转发给调用者,或者使调用者对这样发生的事情_负责_。

现在,事实是,我们在工业界已经了解到,从(位于某些不相关模块的静态作用域深处的)操作系统拨出电话是一个糟糕的主意。我们已经被烧伤过很多次,所以我们拒绝了制造这种混乱的_能力_。甚至发誓放弃它。有一个非常清晰的模式:应用程序的 main 函数获取资源,并将它们传递给一组工作对象。测试创建模拟资源,并以完全相同的方式将它们传递给工作对象。

更一般地说:为为你工作的对象提供适当的工具;不要期望这些对象自己制造工具。这种方法产生了小的、可组合的单元,这些单元很容易与系统中肮脏、危险和困难的部分隔离地进行测试。有些人用“依赖注入”这个高大上的名字来称呼它。我称之为“传递参数”。

偶尔会有人——通常在 5-10 年的经验范围内——抱怨传递所有这些参数的所谓负担。

The Company 犯了动态作用域的错误,并将其推向了完美的逻辑极端,这意味着你不能相信一个类定义来描述你调用一个方法时运行的代码。有人可能已经调整了它。然后你可以传递不同对象上的一组调整,并将这些调整集一层又一层地叠加,或者像用过的鼻涕一样打包起来,留着以后用。上帝保佑你去推断该代码的文本。你必须_一次理解所有内容_才能_理解任何内容_。

但是,这个特定的平台只托管了 8 GB 的应用程序源代码。压缩的。 那有什么问题呢?

当我第一次加入_The Company_时,我发现自己在一个喝了这种 kool-aid 的团队中。一旦我建立了一点声誉,我的首要任务就是结束所有这些动态作用域的突变垃圾。我说从今天开始,如果你编写一个需要服务的函数或类,我们将传入一个可以提供该服务的对象。我们_不能_直接从全局环境中获取服务!唯一的例外是可执行过程的 main 函数,原因很明显,我们无法控制调用者。

就在那时,我在另一个时区的新朋友站出来,反对所有这些“额外”参数的所谓负担。

问题是,涉及任何特定服务对象的调用图永远不会既深又宽。更新一些构造函数的签名及其少数几个调用者并不是什么大问题。相反,如果某个新参数需要流经许多函数,那么它可能_真的_需要沿着某个 selfthis(选择你的口味)一起传递。因此,随着对应用程序的任何部分投入关注,时间流逝,新的参数被传递进去。不久之后,特定的参数组表明自己代表了连贯的新对象类型。那时,参数列表开始_缩小_到比原来更短。

如果你的过程有 10 个参数,你可能遗漏了一些。—— Alan Perlis

快进。两年后。我们的团队的应用程序中没有更多的调整层。所有东西都接受它完成工作所需的参数。而且开发人员很高兴! 也更有生产力。

支持者认为 effect-type system 控制着一切,我应该只看推断的类型来理解我的程序。

我认为这里适用一些 Python 之禅的规则:

结论:

这只是一些模糊相关的概念的集合。可能并不总是清楚一个部分中提出的观点如何与下一个部分中表达的想法联系起来。我将这件事委托给你的认知推理算法。请给我一份我所犯的任何疏漏的报告。

也许这篇文章已经达到了预期的效果?

感谢评论的澄清:

围绕 checked exceptions 的工业功能障碍可能来自这样一个事实,即它们与注入的行为不兼容。Inject-ee 突然可以抛出 inject-ed 可以抛出的所有东西。Inject-or 完全能够处理 inject-ed 可能抛出的任何东西,但 inject-ee 不应该需要考虑这些事情。使用 checked-exceptions 如 Java 中那样, inject-ee 似乎需要关于并非其适当关注的事情的静态注释。

我也可能会取消传统的 exceptions。只需将你的 catch 子句变成一个你作为参数传入的对象上的一个方法。这个提议摆脱了 exceptions 的继承层次结构。我认为这是一件好事,但如果你不这么认为,那么我也很乐意将_捕获 exceptions 的能力_视为你可以传递的一流对象。

Java 支持基于参数类型的方法重载。它基于最具体的规则进行解析。你可以声明一个标准方法名称“handle”,带有一个参数:exception。这将重新获得 exceptions 的类型层次结构的想法。你甚至可以显式地链接 exception handlers……但我离题了。

过程是从 <input, environment> 到 的全函数。它们不能以其他方式存在。商业中的许多人喜欢以其他方式思考:一个函数可能会引发一个 exception。(或一个 effect。)现在他们认为这个过程的工作完成了。但它还没有完成。调用该过程的净结果是根据当时动态作用域中碰巧存在的 exception handler 来参数化的。在可恢复的 exceptions 的上下文中,这可能更清楚地是正确的。

我们已经了解到,动态作用域几乎永远不是你想要的。它在小型玩具示例中很好,但它很快就会失控。

我实际上确实想要 checked exceptions 的 checked -ness。我只是认为 Java 在错误的地方强制检查是错误的。如果我在我没有创建的某个对象上调用一个公共方法,那么我不应该对它是否崩溃负责!检查应该不适用于谁调用该方法,而适用于谁创建可能抛出该方法的对象。(这就是解决问题的方法所在——或者最好是。)

使用 Dinky 主题托管在 GitHub Pages