在模块(分发?)级别强制执行 API 边界
Posted
技术标签:
【中文标题】在模块(分发?)级别强制执行 API 边界【英文标题】:Enforcing API boundaries at the Module (Distribution?) level 【发布时间】:2021-06-13 08:17:12 【问题描述】:我如何构建 Raku 代码,以便某些符号在我正在编写的库中在中是公开的,但对库的用户不公开? (我说“库”是为了避免术语“分发”和“模块”,文档有时会以重叠的方式使用它们。但如果我应该使用更精确的术语,请告诉我。)
我了解如何在单个文件中控制隐私。例如,我可能有一个文件Foo.rakumod
,其内容如下:
unit module Foo;
sub private($priv) #`[do internal stuff]
our sub public($input) is export #`[ code that calls &private ]
使用此设置,&public
是我图书馆公共 API 的一部分,但 &private
不是 - 我可以在 Foo
中调用它,但我的用户不能。
如果&private
变得足够大以至于我想将其拆分到自己的文件中,我该如何保持这种分离?如果我将&private
移动到Bar.rakumod
,那么我需要从Bar
模块给它our
(即包)范围和export
,以便能够从@use
它987654336@。但是这样做与我从Foo
导出&public
的方式相同,将导致我的库的用户能够use Foo
并调用&private
——这正是我试图避免的结果。如何维护&private
的隐私?
(我通过在 META6.json 文件中将 Foo
列为我的 distribution provides
的模块来研究强制隐私。但从文档中,我的理解是 provides
控制像 zef 安装的模块包管理器默认但实际上并不控制代码的隐私。对吗?)
[编辑:我得到的前几个回复让我怀疑我是否遇到了XY problem。我以为我在问“简单的事情应该是简单的”类别中的一些事情。我是从 Rust 背景来解决 API 边界的问题,common practice 是在 crate 中公开模块(或仅对它们的父模块)——这就是我问的 X。但如果有更好/不同的方式在 Raku 中强制执行 API 边界,我也会对该解决方案感兴趣(因为这是我真正关心的 Y)]
【问题讨论】:
我不相信你可以。要做的一件事可能是(a)将它们标记为is implementation-detail
(这表明这里是龙)并且(b)仅通过导出提供带有包密钥的子,例如use Foo::Secret :I-hereby-understand-that-foo-secret-is-designed-for-internal-use-only-and-agree-to-in-hold-the-module-author-harmless-for-any-and-all-damages-thereby-caused-in-sæcula-sæculorum
,或者类似的巧妙写法(你毕竟是律师)
另一种选择可能是将 EVAL
从资源文件转换为 my
范围值,但我在将代码块存储在预编译文件中时遇到了问题,因此您可能必须在运行时完成,失去你无疑想要的好处(但也许 jnhtn 的新调度东西会产生修复它的副作用)
【参考方案1】:
正如其他人所说,没有办法强制执行此 100%。 Raku 只是为用户提供了太多的灵活性,让您能够在外部完美地隐藏实现细节,同时仍然在内部文件之间共享它们。
但是,您可以使用如下结构非常接近:
# in Foo.rakumod
use Bar;
unit module Foo;
sub public($input) is export #`[ code that calls &private ]
# In Bar.rakumod
unit module Bar;
sub private($priv) is export is implementation-detail
unless callframe(1).code.?package.^name eq 'Foo'
die '&private is a private function. Please use the public API in Foo.'
#`[do internal stuff]
当从Foo
的主线中声明的函数调用时,该函数将正常工作,但如果从其他地方调用,则会抛出异常。 (当然,用户可以捕捉到异常;如果你想阻止这种情况,你可以exit
代替——但是有决心的用户可以覆盖&*EXIT
处理程序!正如我所说,Raku 为用户提供了很大的灵活性) .
不幸的是,上面的代码有运行时开销并且相当冗长。而且,如果您想从更多位置致电&private
,它会变得更加冗长。因此,在大多数情况下,将私有函数保存在同一个文件中可能会更好——但这个选项存在于需要时。
【讨论】:
“正如其他人所说,没有办法强制执行此 100%。Raku 只是为用户提供了太多的灵活性”该摘要似乎具有极大的误导性。你可以说 any PL 一样的话。 Haskell 模块将受到完全相同的考虑;正如 jnthn 所指出的——作为一个关键点,而不是一个丢弃点——人们总是可以剪切/粘贴代码。那么PL能做什么呢? Raku 与你所说的完全相反。它提供了图灵完备 GPL 的灵活性,也就是说完全的灵活性,这对任何其他 GPL 都是如此,但限制什么是通常可用的。 为了避免运行时间成本,你可以在 Foo 中使用my &private = Bar::get-private; sub public ($input) is export private
,然后在 Bar 中使用 my sub private … ; method get-private is implementation-detail die "NO" unless callframe(1).code.?package.^name eq 'Foo'; &private
(未经测试的代码,可能是那里的错字)。这样运行时间成本就会小得多(只需检查一次堆栈)。
“确定的用户可以覆盖&*EXIT
处理程序!”。即使用户可以不重载&*EXIT
处理程序,他们也可以复制您的所有模块,进行他们希望进行的任何更改,甚至以完全相同的名称将它们发布到生态系统中。
“作为关键点,而不是丢弃点——人们总是可以剪切/粘贴代码。” @raiph,我不同意这是关键(正如我之前对 jnthn 提到的)。是的,有人可以分叉代码,但是他们有一个不同的程序,可以用它做任何他们想做的事情。 API 隐私是关于不破坏用户代码,复制粘贴我的代码的人永远不会因为我所做的任何更改而破坏代码。而且,当然,警告可以帮助阻止人们依赖实施细节。但是,归根结底,他们仍然会这样做,而且我仍然不想破坏他们的代码。
“我不同意这是关键(正如我之前对 jnthn 提到的)。”这很公平。但我认为你错了,而是同意 jnthn。 “复制粘贴我的代码的人永远不会因为我所做的任何更改而破坏他们的代码。”如果他们剪切/粘贴,然后再次这样做以跟踪您的更改,他们的代码可能会由于您的更改而中断。如果开发人员故意忽略您的公共 API,他们会故意违反合同。你不能阻止他们做他们想做的任何事情。您所能做的就是明确合同的结束。您可以添加律师,也可以添加警察部队,但为什么呢?【参考方案2】:
我需要给它我们的(即包)范围并从 Bar 模块中导出它
第一步是不必要的。 export
机制同样适用于词法范围的 sub
s,这意味着它们仅对导入它们的模块可用。由于没有隐式的重新导出,模块用户必须明确使用包含实现细节的模块才能让它们触手可及。 (顺便说一句,就我个人而言,我几乎从不在我的模块中使用 our
作用域作为 subs,并且完全依赖于导出。但是,我明白为什么人们也可能决定以完全限定的名称提供它们。)
还可以对内部事物使用导出标签(is export(:INTERNAL)
,然后是 use My::Module::Internals :INTERNAL
),以向模块用户提供更强有力的提示,即他们正在取消保修。归根结底,无论语言提供什么,有足够决心重用内部的人都会找到一种方法(即使它是从您的模块中复制粘贴)。一般来说,Raku 的设计重点是让人们更容易做正确的事情,而不是让他们不可能“错误”地做事,因为有时错误的事情仍然比其他选择错误更少.
【讨论】:
> “无论语言提供什么,有足够决心重用内部结构的人都会找到方法(即使是从你的模块中复制粘贴)。”这是一个完全公平的观点。我想我是从Hyrum's law 的角度来看的:如果有人复制粘贴我的代码(我希望他们这样做,或者至少阅读它!),那么他们就有一个 fork,而且我的实现细节的更改不会破坏他们的代码。但是,如果他们忽略了我不使用某些东西的“强烈提示”,那么我可以打破它们——从海伦姆定律的角度来看,这现在是我的 API 的一部分。 我仍然偏爱我的法律免责声明导出标签 ;-)【参考方案3】:事实上,你不能做的事情很少,只要你能控制meta-object protocol。任何在语法上可能的事情,原则上您都可以使用一种特定类型的方法或类,使用它声明。例如,您可以有一个 private-class
,它只对同一命名空间的成员可见(您将设计的级别)。 Metamodel::Trusting
为特定实体定义了它信任的对象(请记住,这是实现的一部分,而不是规范,可能会发生变化)。
一种不太可扩展的方法是使用trusts
。新的私有模块需要是类,并为每个可以访问它的类发出一个trusts X
。这可能包括属于同一分布的类……与否,由您决定。正是上面的 Metamodel 类提供了这个特性,所以直接使用它可能会给你更高级别的控制(使用更低的编程水平)
【讨论】:
使用trust
(和Metamodel::Trusting
)非常困难,因为必须在编译时完成并且需要某种类型的前向声明(请参阅tio.run/##RY6xCsIwFEX3fMUtZGiXDCIOKQ4pDgqOipsS2icdWluaiJXSf/Fb/…了解它在运行时的样子— 但是,它不起作用,因为 ^add_trustee
在运行时是无操作的)
@user0721090601 但我认为 OP 没有明确提及运行时。从理论上讲,既然是在分发级别完成的,那么它可以在编译时完成,对吧?
编译时间很棘手,因为每个模块都需要引用另一个。这不可能完全在编译时完成,因为它会创建循环依赖。也许在 CHECK 阶段,您可以在主(非私有)模块中进行间接引用,但我不确定该引用的效果如何。 (我没有测试过)
@user0721090601 你仍然可以在运行时使用 MOP...以上是关于在模块(分发?)级别强制执行 API 边界的主要内容,如果未能解决你的问题,请参考以下文章