好的,这会有点棘手。
在 Rebol 3 中没有系统词之类的东西,只有词。一些单词已添加到运行时库lib
中,并且set
是其中一个单词,恰好有一个函数分配给它。模块从 导入单词lib
,尽管“导入”的含义取决于模块选项。这可能比你预期的更棘手,所以让我解释一下。
常规模块
对于初学者,我将讨论导入对于“常规”模块的含义,即那些没有指定任何选项的模块。让我们从您的第一个模块开始:
REBOL [Type: 'module] set 'foo "Bar"
首先,您在这里有一个错误的假设:这个词foo
不是模块的本地词,它与set
. 如果要定义foo
为本地词,则必须使用与对象相同的方法,在顶层使用该词作为集合词,如下所示:
REBOL [Type: 'module] foo: "Bar"
foo
和之间的唯一区别是您set
尚未导出或添加单词。当您在模块中引用未声明为本地单词的单词时,它必须从某个地方获取它们的值和/或绑定。对于常规模块,它将代码绑定到第一个,然后通过再次将代码绑定到模块的本地上下文来覆盖它。在本地上下文中定义的任何单词都将绑定到它。任何未在本地上下文中定义的单词都将保留其旧绑定,在本例中为. 这就是“导入”对于常规模块的含义。foo
lib
lib
lib
在您的第一个示例中,假设您自己没有这样做,则该词foo
没有提前添加到运行时库中。这意味着 thatfoo
没有绑定到lib
,并且由于它没有被声明为本地单词,因此它也没有绑定到本地上下文。因此,结果,foo
根本不受任何约束。在您的代码中这是一个错误,但在其他代码中可能不是。
隔离模块
有一个“隔离”选项可以改变模块导入内容的方式,使其成为“隔离”模块。让我们在这里使用您的第二个示例:
REBOL [Type: 'module Options: [isolate]] set 'foo "Bar"
当创建一个独立的模块时,模块中的每个单词,即使是嵌套代码,都会被收集到模块的本地上下文中。在这种情况下,它意味着set
和foo
是本地词。这些单词的初始值被设置为它们在lib
创建模块时所具有的任何值。也就是说,如果这些词是lib
在所有定义的。如果单词在 中没有值lib
,则它们最初在模块中也没有值。
重要的是要注意,这种价值观的导入是一次性的。在初始导入之后,在模块外对这些单词所做的任何更改都不会影响模块中的单词。这就是为什么我们说模块是“孤立的”。对于您的代码示例,这意味着有人可以更改lib/set
并且不会影响您的代码。
但是您错过了另一种重要的模块类型......
脚本
在 Rebol 3 中,脚本是另一种模块。这是您作为脚本的代码:
REBOL [] set 'foo "Bar"
或者,如果您愿意,因为脚本头在 Rebol 3 中是可选的:
set 'foo "Bar"
脚本也从 导入它们的单词lib
,并将它们导入到一个孤立的上下文中,但有一点不同:所有脚本共享相同的孤立上下文,称为“用户”上下文。这意味着当您更改脚本中某个单词的值时,下一个使用该单词的脚本将在启动时看到更改。因此,如果在运行上述脚本后,您尝试运行此脚本:
print foo
然后它将打印“Bar”,而不是foo
未定义,即使foo
在lib
. 如果您以交互方式使用 Rebol 3,在控制台中输入命令并获得结果,您可能会觉得有趣,您输入的每个命令行都是一个单独的脚本。因此,如果您的会话如下所示:
>> x: 1
== 1
>> print x
1
x: 1
和行是单独的print 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/bar
foo
值得注意的是,这种自动导出只发生一次,当主体foo
完成执行时。如果您在此之后进行更改foo/bar
,则不会影响lib/bar
. 如果您也想更改lib/bar
,则必须手动进行。
还值得注意的是,如果在导入lib/bar
之前已经存在foo
,则不会添加另一个单词。如果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 的模块系统的设计有足够的了解。