本 EEP 引入了一个名为 internal_export
的新指令,该指令可以实现函数导出的语义分离。本 EEP 的灵感主要来自 EEP 5。
Erlang 应用程序 API 是一个含义模糊的术语。理论上,它是一组模块 API(导出的函数)。实际上,它是一组模块的 API,但没有被认为是内部的、未文档化的导出、回调实现等。在阅读文档时,您可以得出什么是应用程序 API,但在阅读源代码时,它并不那么直观。
并非所有的导出在语义上都是相同的。一个函数可以被导出以作为应用程序 API 的一部分、实现回调、测试某些代码或允许应用程序内的模块使用它。目前,从用户的角度来看,它们都是一样的。存在一个不使用未文档化的导出的约定,但是没有办法在代码库中强制执行该约定。
本 EEP 的目标是提供一种机制,将应用程序 API 导出与其他导出分离开来。
为方便起见,引入了几个术语
internal_export
属性标记的函数.app
文件中 applications
、include_applications
和 optional_applications
属性(及其依赖项,递归地)中找到的应用程序的集合内部导出的函数仍然是全局导出的,但不鼓励应用程序用户使用它们。该机制类似于弃用机制,您可以调用已弃用的函数,尽管不建议这样做。您还可以使用一种机制来检查是否有对该函数的静态调用,因此您不需要手动检查每个函数是否已弃用。
内部导出函数的语法如下
-internal_export([f/a, ...]).
其中 f
是 atom()
,a
是 arity()
。
内部导出的函数可以合法地从其应用程序内部及其依赖项中调用。应用程序的依赖项可以合法调用内部导出函数的原因是回调到其他应用程序1。
应用程序 A
包含模块 mod_A
,该模块导出 public/1
并内部导出 internal/1
。
应用程序 B
包含模块 mod_B
,该模块导出 x/1
并内部导出 y/1
。
应用程序 B
依赖于应用程序 A
。
mod_A:internal/1
只能合法地从 A
内部调用,而 mod_B:y/1
可以合法地从 A
和 B
调用。从 A
到 mod_B:y/1
的静态调用可能永远不会发生,因为 A
不知道 B
,但可能 mod_B:y/1
是 mod_A
的回调实现。所有其他应用程序都不能合法调用 mod_A:internal/1
和 mod_B:y/1
。
尽管想法相似,但本 EEP 与 EEP 5 在设计和实现上存在一些关键差异
存在一个开放的问题,即将函数导出到模块还是应用程序。目前在 EEP 5 中提出了导出到模块。
尽管基于模块的方法很简单,但它有两个缺点
导出到行为 - 您会期望
-export_to(gen_server, [init/1, handle_cast/2, handle_call/3]).
会起作用。
以这种方式导出回调会非常方便,而不是
%% gen_server API
-export([init/1, handle_cast/2, handle_call/3]).
,但这种表示法实际上是错误的,因为对 gen_server
回调的调用是在 gen
模块中完成的,而不是在 gen_server
中完成的。用户甚至不应该考虑该实现细节,更不用说编写依赖于它的代码了。
目前,基于注释是澄清模块/应用程序 API 的一种广泛使用的方法,例如
%% gen_server
-export([init/1, handle_cast/2, handle_call/3, code_change/3]).
%% system calls
-export(...).
%% test-only
-export(...).
%% API
-export(...).
%% internal exports
-export(...).
当然,这可行,但是基于注释的导出的语义分离限制了代码分析工具的任何使用。我们能做得更好吗?
在 EEP 5 中,声明导出范围的责任在于程序员。这可能会产生方便的、语义价值更高的代码,例如
-export_to([mod_x, mod_y], [f_1/1, f_2/0]).
它表示某些特定导出的目的,即哪些模块可以调用它们。请注意,这仅在将函数使用限制为应用程序本身(通常称为内部导出)时才方便 - 前面提到过,当将函数导出到行为模块时,这种方法不适用。
这提出了一个问题:我们真正需要/想要多少控制? 本 EEP 旨在限制用户滥用应用程序/模块的方式。滥用不是来自应用程序本身,也不是来自其依赖的应用程序,而是来自其用户!也就是说,除了私有和全局之外,还需要内部导出范围。
无需用户手动指定范围,因为它可以在编译时确定:内部导出的函数可以从声明它的应用程序或其任何依赖的应用程序中调用。
隐式范围不如显式范围具有语义价值,但它完成了主要任务;它将应用程序/模块的公共 API 与内部内容分离开来。
当与显式范围一起使用时,export_to
属性看起来完全合乎逻辑,但是对于隐式范围,不清楚 _to 指的是什么。建议使用名称 internal_export
而不是 export_to
。请注意,它只有一个参数,其语法规则与 export
相同。
在 EEP 5 中,调用将在加载器中(对于静态调用)和运行时(对于动态调用)进行检查,如果发生无效调用,则会导致失败。相比之下,本 EEP 建议采用基于代码分析的方法,使用 xref
检查所有静态调用并忽略所有动态调用。对所有函数的调用在运行时仍然有效。当然,这种方法不提供运行时保证,但不会产生性能影响,并且需要更少的工作和维护。
当前的实现位于 OTP 存储库中的 PR 7407 中。
除了此 PR 之外,还有关于 对内部导出的需求 和 实现 的论坛帖子。
已经使用 -internal_export(FAs).
属性的代码将受到影响。
重要的是要说明这些回调可以合法地从依赖应用程序中使用。如果有一天动态检查成为现实,机制可能会保持不变。也就是说,当前的机制可以简化,并且只检查内部导出是否仅在同一个应用程序内使用。
本文档置于公共领域或 CC0-1.0-Universal 许可之下,以较宽松的为准。