Rebol 模块中的单词是如何绑定的?

Posted

技术标签:

【中文标题】Rebol 模块中的单词是如何绑定的?【英文标题】:How are words bound within a Rebol module? 【发布时间】:2013-01-03 11:06:52 【问题描述】:

我知道module! 类型为受保护的命名空间提供了比object!'use 函数更好的结构。模块中的单词是如何绑定的——我注意到一些与未绑定单词相关的错误:

REBOL [Type: 'module] set 'foo "Bar"

另外,Rebol 如何区分模块本地的单词 ('foo) 和系统函数的单词 ('set)?

小更新,不久之后:

我看到有一个开关改变了绑定方法:

REBOL [Type: 'module Options: [isolate]] set 'foo "Bar"

这有什么不同?默认使用这种方法有什么问题?

【问题讨论】:

【参考方案1】:

好的,这会有点棘手。

在 Rebol 3 中没有系统词之类的东西,只有词。运行时库lib 中添加了一些单词,set 是其中之一,恰好有一个函数分配给它。模块从lib 导入单词,但“导入”的含义取决于模块选项。这可能比您预期的要复杂,所以让我解释一下。

常规模块

首先,我将讨论导入对于“常规”模块的含义,即那些没有指定任何选项的模块。让我们从您的第一个模块开始:

REBOL [Type: 'module] set 'foo "Bar"

首先,你在这里有一个错误的假设:单词foo 不是模块本地的,它与set 相同。如果您想将foo 定义为本地词,您必须使用与对象相同的方法,在顶层使用该词作为集合词,如下所示:

REBOL [Type: 'module] foo: "Bar"

fooset 之间的唯一区别是您尚未将单词 foo 导出或添加到 lib。当您在模块中引用未声明为本地单词的单词时,它必须从某个地方获取它们的值和/或绑定。对于常规模块,它首先将代码绑定到lib,然后通过再次将代码绑定到模块的本地上下文来覆盖它。在本地上下文中定义的任何单词都将绑定到它。任何未在本地上下文中定义的单词都将保留其旧绑定,在本例中为 lib。这就是“导入”对于常规模块的意义。

在您的第一个示例中,假设您自己没有这样做,foo 这个词并没有提前添加到运行时库中。这意味着foo 没有绑定到lib,并且由于它没有被声明为本地单词,它也没有绑定到本地上下文。因此,foo 根本不受任何约束。在您的代码中这是一个错误,但在其他代码中可能不是。

隔离模块

有一个“隔离”选项可以改变模块导入内容的方式,使其成为“隔离”模块。让我们在这里使用您的第二个示例:

REBOL [Type: 'module Options: [isolate]] set 'foo "Bar"

当创建一个独立的模块时,模块中的每个单词,即使是嵌套代码,都会被收集到模块的本地上下文中。在这种情况下,这意味着setfoo 是本地词。这些单词的初始值设置为它们在创建模块时在lib 中的任何值。也就是说,如果这些词完全在lib 中定义。如果单词在lib 中没有值,则它们最初在模块中也没有值。

重要的是要注意,这种价值观的导入是一次性的。在初始导入之后,在模块外对这些单词所做的任何更改都不会影响模块中的单词。这就是为什么我们说模块是“孤立的”。在您的代码示例中,这意味着有人可以更改 lib/set 而不会影响您的代码。

但是你错过了另一个重要的模块类型......

脚本

在 Rebol 3 中,脚本是另一种模块。这是您的脚本代码:

REBOL [] set 'foo "Bar"

或者,如果您愿意,因为脚本头在 Rebol 3 中是可选的:

set 'foo "Bar"

脚本还从lib 导入它们的单词,并将它们导入一个孤立的上下文,但有一点不同:所有脚本共享相同的孤立上下文,称为“用户”上下文。这意味着当您更改脚本中某个单词的值时,使用该单词的 next 脚本将在启动时看到更改。所以如果在运行了上面的脚本之后,你尝试运行这个:

print foo

然后它将打印“Bar”,而不是使foo 未定义,即使foo 仍未在lib 中定义。如果您以交互方式使用 Rebol 3,在控制台中输入命令并获得结果,您可能会觉得有趣,您输入的每个命令行都是一个单独的脚本。因此,如果您的会话如下所示:

>> x: 1
== 1
>> print x
1

x: 1print x 行是单独的脚本,第二行利用了第一行对用户上下文所做的更改。

用户上下文实际上应该是任务本地的,但现在让我们忽略它。

为什么会有差异?

这是我们回到“系统功能”的地方,而 Rebol 没有它们。 set 函数与任何其他函数一样。它可能以不同的方式实现,但它仍然是分配给正常单词的正常值。应用程序必须管理很多这些词,这就是我们有模块和运行时库的原因。

在一个应用程序中会有一些需要改变的东西,而其他一些需要改变的东西,哪些东西取决于应用程序。您将需要对您的东西进行分组,以使事情井井有条或进行访问控制。会有全局定义的东西和本地定义的东西,你会希望有一种有组织的方式来将全局的东西带到本地,反之亦然,并在不止一个东西想要定义东西时解决任何冲突同名。

在 Rebol 3 中,为了方便和访问控制,我们使用模块对内容进行分组。我们使用运行时库lib 作为收集模块导出和解决冲突的地方,以便控制导入到本地的内容,如其他模块和用户上下文。如果您需要覆盖某些内容,您可以通过更改运行时库来完成此操作,并在必要时将您的更改传播到用户上下文。您甚至可以在运行时升级模块,并让新版本的模块覆盖旧版本导出的单词。

对于常规模块,当事物被覆盖或升级时,您的模块将从这些更改中受益。假设这些变化是有益的,这可能是一件好事。常规模块与其他常规模块和脚本协作,形成一个共享环境来工作。

但是,有时您需要远离这些变化。也许您需要某个功能的特定版本并且不想升级。也许您的模块将被加载到一个不太值得信赖的环境中,并且您不希望您的代码被黑客入侵。也许你只是需要让事情变得更可预测。在这种情况下,您可能希望将您的模块与这些类型的外部更改隔离开来。

隔离的不利之处在于,如果您可能想要对运行时库进行更改,那么您将无法获得它们。如果您的模块可以以某种方式访问​​(例如通过名称导入),那么有人可能能够将这些更改传播给您,但如果您无法访问,那么您就不走运了。希望您考虑过监视 lib 以进行您想要的更改,或者直接通过 lib 引用这些内容。

不过,我们错过了另一个重要问题...

导出

管理运行时库和所有这些本地上下文的另一部分是导出。你必须以某种方式把你的东西拿出来。最重要的因素是您不会怀疑的:您的模块是否有名称。

名称对于 Rebol 3 的模块是可选的。起初,这似乎只是一种简化模块编写的方法(在 Carl 的原始提案中,这正是原因)。然而,事实证明,当你有一个名字时,你可以做很多事情,而当你没有名字时,你就做不到,这仅仅是因为名字是什么:一种引用某物的方式。如果你没有名字,你就无法指代某物。

这似乎是一件微不足道的事情,但这里有一些名字可以让你做的事情:

您可以判断模块是否已加载。 您可以确保一个模块只加载一次。 您可以判断是否存在较早版本的模块,并可能对其进行升级。 您可以访问之前加载的模块。

当 Carl 决定将名称设为可选时,他给我们提供了一种情况,即可以创建您无法执行任何这些操作的模块。鉴于模块导出的目的是在运行时库中收集和组织,我们遇到了这样一种情况,即您可能会对无法轻松检测到的库产生影响,并且每次导入时都会重新加载模块。

因此,为了安全起见,我们决定完全删除运行时库,只将这些未命名模块中的单词直接导出到正在导入它们的本地(模块或用户)上下文。这使得这些模块实际上是私有的,就好像它们归目标上下文所有。我们处理了一个可能很尴尬的情况,并将其作为一项功能。

我们决定使用private 选项明确地支持它。将此作为显式选项有助于我们处理没有名称导致的最后一个问题:使私有模块不必一遍又一遍地重新加载。如果你给一个模块一个名字,它的导出仍然可以是私有的,但它只需要它正在导出的内容的一个副本。

但是,命名与否,私有与否,即 3 种导出类型。

常规命名模块

让我们来看看这个模块:

REBOL [type: module name: foo] export bar: 1

导入这个会将一个模块添加到加载的模块列表中,默认版本为 0.0.0,并将一个单词 bar 导出到运行时库。在这种情况下,“导出”意味着将单词 bar 添加到运行时库(如果它不存在),并将该单词 lib/bar 设置为单词 foo/barfoo 完成执行后所具有的值(如果它尚未设置)。

值得注意的是,这种自动导出只发生一次,当foo 的主体执行完毕时。如果您在此之后对foo/bar 进行更改,则不会影响lib/bar。如果您也想更改lib/bar,则必须手动进行。

另外值得注意的是,如果lib/barfoo被导入之前已经存在,你就不会再添加一个词了。如果lib/bar 已经设置为一个值(不是未设置),导入foo 不会覆盖现有值。先到先得。如果您想覆盖现有的 lib/bar 值,则必须手动执行此操作。这就是我们使用lib 管理覆盖的方式。

运行时库给我们的主要优势是我们可以在一个地方管理所有导出的单词,解决冲突和覆盖。但是,另一个优点是大多数模块和脚本实际上不必说明它们正在导入什么。只要在运行时库中提前正确填写了您需要的所有单词,您稍后加载的脚本或模块就可以了。这使得在您的启动代码中放置一堆导入语句和任何覆盖变得容易,从而设置了其余代码所需的一切。这是为了让组织和编写应用程序代码变得更容易。

命名私有模块

在某些情况下,您不想将内容导出到主运行时库。 lib 中的内容会被导入到所有内容中,因此您应该只将想要公开的内容导出到 lib。有时你想制作只为需要它的上下文导出东西的模块。有时你有一些相关的模块,一个通用的工具和一个实用模块左右。如果是这种情况,您可能需要创建一个私有模块。

让我们来看看这个模块:

REBOL [type: module name: foo options: [private]] export bar: 1

导入此模块不会影响lib。相反,它的导出被收集到一个私有运行时库中,该运行时库对于正在导入此模块的模块或用户上下文以及目标正在导入的任何其他私有模块的本地运行时库,然后从那里导入到目标。私有运行时库用于与lib 相同的冲突解决。主运行时库lib 优先于私有库,所以不要指望私有库覆盖全局事物。

这种东西对于制作实用模块、高级 API 或其他此类技巧很有用。它对于制作需要显式导入的强模块化代码也很有用,如果你喜欢的话。

值得注意的是,如果您的模块实际上没有导出任何内容,那么命名私有模块和命名公共模块之间没有区别,因此它基本上被视为公共模块。重要的是它有一个名字。这让我们...

未命名的模块

如上所述,如果您的模块没有名称,那么它几乎必须被视为私有。不仅仅是私人的,因为您无法判断它是否已加载,因此您无法升级它甚至无法重新加载它。但如果这是你想要的呢?

在某些情况下,您确实希望您的代码运行有效。在这些情况下,每次都重新运行代码是您想要做的。也许这是您使用do 运行的脚本,但将其构建为模块以避免泄漏单词。也许你正在制作一个 mixin,一些实用函数,它们有一些需要初始化的本地状态。它可以是任何东西。

我经常将我的%rebol.r 文件设为一个未命名的模块,因为我想更好地控制它导出的内容和方式。另外,因为它是为了效果而完成的,不需要重新加载或升级,所以给它起个名字是没有意义的。

不需要代码示例,您之前的代码示例会这样做。

我希望这能让你对 R3 的模块系统的设计有足够的了解。

【讨论】:

您能否详细说明从模块中导出单词和“私有选项” 完成。如果您需要更多信息,请随时询问。 相当全面,谢谢! 你是布莱恩,布莱恩。不错的文章。 我不知道我怎么错过了这么久。信息量很大。

以上是关于Rebol 模块中的单词是如何绑定的?的主要内容,如果未能解决你的问题,请参考以下文章

如何在路由中使用 ngx-permission 模块将特定组件重新绑定到 angular5 中的特定用户

从__init__.py中的函数导入模块将模块对象绑定到全局命名空间?

库模块中的数据绑定

OpenCV-Python绑定如何工作及如何扩展新模块到Python

子模块的故障安全注入器。 google guice 中的可选绑定

Android 数据绑定:如何避免“找不到 KaptTask”警告