作者
Richard A. O'Keefe <ok(at)cs(dot)otago(dot)ac(dot)nz>
状态
草案
类型
标准跟踪
创建于
2008年7月15日
Erlang 版本
OTP_R12B-4

EEP 15:可移植的 fun #

摘要 #

当前的 Erlang 有两种 fun。一种 “外部” fun,Module:Name/Arity,只是一个名称,可以自由使用。“本地” fun 包含绑定到其定义模块的代码。这意味着您不能将内部 fun 保存在数据库中或将其发送到远程系统并期望它们工作。

我提出一种 “可移植的 fun”,它是一种语法受限的 fun。该限制确保程序员知道(并且运行时可以发现)确切需要哪些模块。这些 fun 可以安全地发送到远程节点,并且可以安全地存储在数据库中,稍后检索并执行。持有对此类 fun 引用的进程也不需要在其来源模块卸载时被终止。

为了获得最佳速度,需要一种新的实现这些 fun 的方法,因此这是一个相当大的改变。但是,解释可移植函数的原型是可能的。

规范 #

目前,Erlang 有

fun_expr -> 'fun' fun_clauses 'end' : ...

我们添加

fun_expr -> 'fun' '!' fun_clauses 'end' : ...

并做出以下限制

  1. 可移植的 fun 不得包含普通 fun。
  2. 可移植的 fun 不得包含没有模块前缀的 f(…) 调用,除非 f 是一个内置函数。
  3. 可移植的 fun 不得包含任何形式为 M:f(…),m:F(…) 或 M:F(…) 的调用。
  4. 可移植的 fun 不得包含任何形式为 F(…) 的调用,除非 F 在其头部绑定。
  5. 在可以使用抽象模式的系统中,它们被限制的方式与函数调用相同。

这些限制的目的是确保每次调用都是调用内置函数、已知模块的已知导出,或者是某种作为参数接收的 fun。

内置函数 erlang:fun_info/1 以以下方式扩展

  1. 在 {type,Type} 项中,Type 可以是 ‘portable’。
  2. 在可移植 fun 的 {module,Module} 项中,Module 将存在,但实际上,可移植 fun 与任何同名模块之间没有任何其他联系。
  3. 在可移植 fun 的 {name,Name} 项中,Name 将始终为 []。
  4. 不会为 ‘portable’ fun 返回为 ‘local’ fun 指定的任何项。
  5. {calls,List} 将为可移植的 fun 返回,其中 List 是 {Module,Imports} 对的列表,其中 fun 中远程调用中使用的每个模块都列出一次,并且 Imports 是 *:module_info/0 中报告的 {Name,Arity} 对的列表。这允许可移植 fun 的接收者确定需要加载哪些模块以及它们应该导出哪些函数。
  6. 为了保持一致性,erlang:fun_info(fun M:F/A, calls) => [{M,[{F,A}]}]

内置函数 erlang:fun_info/2 类似地扩展。为此函数提供了额外的键 ‘source’。

fun_info(Fun, source) #

  • 对于本地 fun,结果为 ‘undefined’。
  • 对于外部 fun,结果是解析器为 fun M:F/A 返回的抽象语法树。
  • 对于可移植 fun,结果是解析器为 fun!… end 形式返回的抽象语法树。

内置函数和保护谓词 erlang:is_function(Term) 和 erlang:is_function(Term, Arity) 接受可移植 fun 以及外部和本地 fun。

提供了两个新的内置函数和保护谓词 erlang:is_portable_function(Term) 和 erlang:is_portable_function(Term, Arity),它们识别 ‘portable’ 和 ‘external’ 函数。

(此提案肯定需要修改以使名称更清晰。)

动机 #

假设您有一个 Erlang 节点向其他节点上的客户端报告事件。客户端希望仅接收其中的几个事件。一种方法是让报告者向所有客户端发送所有事件,并让客户端进行过滤。更好的方法是让客户端告诉报告者他们想要哪些事件,并让它只发送感兴趣的事件。但是,客户端如何告诉报告者他们对哪些事件感兴趣?

一种方法是简单地拥有一组固定的事件类。这太粗糙了。

另一种方法是定义一种事件描述语言,也许在某种程度上基于匹配规范。这更好,但是目前没有办法编译匹配规范(这是它要做的另一件事!)因此匹配速度很慢,而且仍然受到限制;报告者可能希望提供过滤器可以使用的摘要函数。

另一种方法是发送一个 fun,这实际上是执行此操作的明显方式。不幸的是,这目前无法工作,并且存在不应该这样做的原因。(例如,本地函数的主体可能已经受到接收节点上定义不同的函数的内联扩展的影响。)

另一种方法是将整个模块作为二进制文件发送。这有点重量级。它还在报告者中创建了管理可能大量模块的问题。除非报告者做了大量工作来验证代码的安全性,否则它也是不安全的。从长远来看,如果客户端和报告者没有使用完全相同的 BEAM(或其他 VM),它也会产生版本偏差问题。

对于另一个示例,考虑将函数存储在数据库中。由于本地 fun 绑定到特定模块的特定版本,如果您在一个月内保存一个函数,升级您的系统,并在下个月恢复该模块,则您不能期望它工作。这意味着,例如,您不能将二进制文件与知道如何解码它的函数一起存储。

对于另一个示例,考虑像动态接收匹配规范(或类似匹配规范)的数据库,并希望将其应用于数百万条记录。将匹配规范转换为 Erlang 代码,甚至编译结果很容易,但是现在您有一个要管理的模块,而不是一个可以由垃圾收集器清理的简单事物。

基本上,此提案的目的是使 Erlang 在 “函数是数据” 的函数式编程道路上更进一步。

但是,有必要以这样一种方式进行此操作,即接收可移植 fun 的进程不必完全信任源。接收者必须能够检查可移植 fun,而不仅仅是调用它。

原理 #

仅仅在现有 fun 语法之上添加可移植性限制不是一个好主意。这将破坏大多数使用 fun 的程序。

也许显而易见的事情是使用 #fun…end,因为 # 似乎是 Erlang 的 “哎呀,我们在过去的好日子里没有想到” 的标记,就像它在 Common Lisp 中一样。但是,我们想要使用该表示法来表示匿名抽象模式,而且无论如何,在这种情况下,# 没有什么标志性的含义。

感叹号用于表示这是一种您可能想要发送的 fun,事实也确实如此。至于它放置的位置,感叹号应被视为后修改 ‘fun’ 关键字,而不是预修改参数列表,因此

fun!({a,X}) -> ...
   ;({b,Y}) -> ...
end

没有重复的感叹号。

发送可移植 fun 时,您发送什么?

  • 当然是环境
  • 当然是一些标头
  • 但是 CODE 看起来像什么?

如果是本机代码,则无法将 fun 从 SPARC 发送到 Mac。如果是 BEAM 代码,则无法将 fun 发送到另一个系统,除非它具有完全相同的 BEAM 版本。在这两种情况下,您都给想要检查代码的谨慎接收者带来了极大的困难。如果它是源代码,那么

  • 它可以(懒惰地!)编译为 BEAM(或某些其他 VM)
  • 它可以被解释
  • 它可以被调试
  • 它可以被检查
  • 我们不必担心编译器如何处理推导式 – 可悲的是,当前的编译器会生成递归辅助函数,这使事情变得复杂,并且可以采用更好的方法

因此,可移植 fun 的二进制格式将包括源树,可能像 Kistler 的 Juice 中那样压缩。本机表示形式将包括指向 BEAM 代码块的指针,以及可选的指向本机代码块的指针,但是这些指针将在首次调用时填充。

解释的可能性意味着有一种廉价的方法来实现此 EEP 的原型:始终解释。这也反对对现有 fun 进行任何更改;我们不想降低它们的速度。

向后兼容性 #

“fun!” 当前是一个语法错误,因此不会影响任何现有代码。

当我阅读 erlang:fun_info/[1,2] 的文档时,程序员应该始终将这些函数视为开放式的。现有手册中承诺的任何内容都不会被删除或更改,仅提供新值。

任何调用 erlang:is_portable_function/[1,2] 的现有程序都无法工作,因为没有这样的函数。如果模块定义了 is_portable_function/1 或 /2,则不允许在保护中使用,但允许在其他地方使用;这样的模块可能会受到影响。如果编译器在模块中发现任何一个函数的定义,它应该打印警告消息,并且只使用该模块的版本。

参考实现 #

无。

从长远来看,这至少需要两件事

  1. 一种 fun 表示形式,该表示形式在不属于任何模块的二进制文件中保存指令,这与经典的 Interlisp-D 实现非常相似,因此可以单独对这些 fun 进行垃圾回收。这无论如何都是可取的。

  2. 一种用于推导式的编译策略,类似于经典的 Pop-2 系统,它生成内联循环而不是调用离线辅助函数。无论如何,这是可取的;它应该会明显更快。

版权 #

本文档已置于公共领域。