6

我的问题与require与所需命名空间的静态或动态分辨率一起使用时的行为有关。

我将尝试表达我对事物的理解:

[ 1 ] 将“require”与文字一起使用

    { require MODULE; }

在这种情况下,编译器会检查 MODULE 是否已被声明为符号。如果没有,编译器声明它,并将它绑定到一个空的占位符包,它只是为这个“要求”创建的

{
    my $response = ::('MODULE');  # this happens at runtime
    say $response.^name;          # MODULE doesn't exist so the lookup results in the compilation-phase placeholder package: MODULE

    try require MODULE;           # although the execution order of require comes after the lookup, 
                                  # the placeholder package creation was done during compilation and the package is present in the current scope during run-time
}

[ 2 ] 将“require”与字符串一起使用

    { try require 'FILE_PATH'; }

在这种情况下,“require”试图(在运行时)查找由字符串中声明的文件名定义的文件。如果找到(具有适当的内容:模块、包等),则它会在当前范围内创建一个名称空间,并将其与文件的内容一起加载。

[ 3 ] 在动态查找中使用“require”

    { try require ::('MODULE'); }

在我看来,在这种情况下,“require”的行为不是“正常”的子程序。

当我们将“require”与“动态查找”一起使用时,动态查找的核心功能将“融合”在一个新的例程中,该例程的行为与我们预期的不同。

事实是“动态查找”例程的结果不是符号就是失败。

如果“require”的行为类似于“正常”子例程,那么它可以使用的唯一输入将是其后的动态查找的结果(命名空间或失败)。

但事实上,在失败的情况下(作为动态查找的结果),“require”会继续在存储库中搜索适当的包(通常情况下,使用我们为动态查找提供的参数: '模块')。

因此,从这个意义上说,显然“要求”的行为不像“正常”子程序。

根据我的思路,require + 动态查找的组合类似于以下构造:

{ modified_dynamic_lookup('MODULE') :if_symbol_not_found_search_repositories_and_if_appropriate_package_found_create_namespace_and_load_package_contents; }

我关心的是我对案例[3]的理解。

require + 动态查找如何工作?(从分析上讲 - 编译器首先遵循的步骤是什么,然后是运行时?)

[ 后记 ]

我同意@raiph 的观点,“require”不是一个子例程,它与语言深度集成。

从这个意义上说,在 require “指令”之后的“动态查找结构”用于两件事:

  1. 通知编译器构造是“动态的”(所以不要在编译时修复任何东西)

  2. 提供将用于搜索符号、命名空间、文件或存储库内容的字符串

@raiph 表示他认为“require”在成功加载后会进行查找。

我对此的唯一反对意见是,当我们加载同一个库时,“require”不会引发任何异常。

它是否默默地忽略了加载的库?当它可以首先检查同一个命名空间是否已经在使用时,为什么还要费心做这么多工作呢?

相反,当我们假设我们加载了一个不同的库时,它会抛出一个异常:正在使用的符号的“重复定义”。

为了证明我进行了以下操作:

在 ./lib 目录中,我放置了两个库,“foo.pm6”是“foo”的单元定义,其中定义了 A 类:

file "foo.pm6" contents:
-----------------------------------
unit module foo;

class A is export {}

和另一个库“other.pm6”,这次在“foo”的定义中定义了一个不同的类 B。

file "other.pm6" contents:
-----------------------------------
module foo {
    class B is export {}
}

raku 程序文件包含以下内容:

use lib <lib>;

my $name = 'other';           # select one of {'other', 'foo'}

require ::('foo') <A>;        ########> Initial package loading

my $a = try ::('foo::A').new;
say '(1) ' ~ $a.^name;        # (1) foo::A

$a = ::('A').new;
say '(2) ' ~ $a.^name;        # (2) foo::A

try require ::($name);        # if $name eq 'other' => throws exception, if $name eq 'foo' => does nothing
with $! {.say};               # P6M Merging GLOBAL symbols failed: duplicate definition of symbol foo ...

$a = try ::('foo::A').new;
say '(3) ' ~ $a.^name;        # (3) foo::A

$a = ::('A').new;
say '(4) ' ~ $a.^name;        # (4) foo::A

从上面的示例中我们看到,当我们尝试重新加载 foo 命名空间时,它隐藏在具有不同名称的文件中(只是为了欺骗 raku),它会引发异常。

因此我得出结论,也许“需要”首先检查与提供的字符串同名的命名空间。

顺便说一句,检查一下,我偶然发现了一个奇怪的行为。如下:

如果我们使用“使用 foo;” 在 line: "Initial package loading" 而不是 "require ::('foo') ;" 中,我们得到以下结果:

(1) foo::A
(2) foo::A
No such symbol 'other' ...

(3) Any
(4) foo::A

在 (3) 中查找 'foo::A' 没有找到任何东西!

此外,如果我使用以下内容更改库文件:“other.pm6”(A 类而不是 B - 如 foo.pm6 中)

file "other.pm6" contents:
-----------------------------------
module foo {
    class A is export {}
}

结果似乎恢复到预期:

(1) foo::A
(2) foo::A
No such symbol 'other' ...

(3) foo::A
(4) foo::A

是错误还是我缺少的其他东西?

4

2 回答 2

4

重写以对应您答案的第三个版本

[ 1 ] 将“require”与文字一起使用

在这种情况下,编译器会检查是否MODULE已经被声明为符号。如果没有,编译器声明它,并将它绑定到一个空的占位符包,它只是为这个“要求”创建的

更具体地说,require 关键字4和它生成的代码起作用了。

它创建符号的唯一原因是人们可以编写该标识符并且代码将编译。如果require不这样做,那么即使编译require FOO成功,使用标识符的代码也将无法编译:

require FOO;
my FOO $bar; # Type 'FOO' is not declared

# MODULE 不存在,因此在编译阶段占位符包中查找结果: MODULE

MODULE 确实存在。并且查找成功。它返回绑定到符号的值,这是在编译阶段放置在那里MODULE的占位符包。require

# 虽然执行顺序require在查找之后

require的编译阶段动作的执行发生在运行阶段的查找之前。

[ 2 ] 将“require”与字符串一起使用**

如果找到(具有适当的内容:模块、包等),则它会在当前范围内创建一个名称空间,并将其与文件的内容一起加载。

我认为符号的唯一声明require是代码编写者明确写为静态标识符作为require语句的一部分。例子:

  • require MODULE <A>; -->MODULEA.

  • require 'MODULE.pm6' <A>;--> A

  • require ::('MODULE') <A>;--> A

Aiui MLS 1作为符号合并(P6M) 的一部分,根据需要声明更多符号。但这项工作不是由require. 它是由 MLS 代表它完成的。而且它并不是require. 作为use语句的结果,在编译阶段发生的工作相同(某种)。

[ 3 ] 在动态查找中使用“require”

{ try require ::('MODULE'); }

我有代码试图证明在尝试加载模块之前不会进行查找。2

在我看来,在这种情况下,“require”的行为不是“正常”的子程序。

require不是例行公事,正常或其他。

say require MODULE;   # Undeclared name:
                            MODULE used at line 1
                      # Undeclared routine:
                            require used at line 1

require如果您在官方文档中搜索,您会发现它没有列在Routine Reference部分,而是列在Language Reference的 Modules 部分中。它是一个关键字,一个语句,是编译器理解的语言的一个特殊部分。

如果“require”的行为类似于“正常”子例程,那么它可以使用的唯一输入将是其后的动态查找的结果(命名空间或失败)。

动态查找的结果是绑定到Symbol的值,如果它被声明的话,或者Failure以其他方式:

my $variable = 42;
say ::('$variable');           # 42
say ::('nonsense') ~~ Failure; # True

$variable不是命名空间。

但事实上,在失败的情况下(作为动态查找的结果),“require”会继续在存储库中搜索适当的包(通常情况下,使用我们为动态查找提供的参数: '模块')。

鉴于我编写的跟踪 ::('MODULE') 2值的动态查找的代码,在我看来require,如果模块加载失败,任何代码(无论是 MLS 还是 MLS)都没有动态查找它。

这反过来意味着它只发生在模块加载期间或之后(如果有的话)。因此,要么 MLS 正在执行此操作(似乎最有可能),要么可能require是在模块成功加载后执行此操作(似乎不太可能,但我还没有准备好 100% 消除它)。

{ modified_dynamic_lookup('MODULE') :if_symbol_not_found_search_repositories_and_if_appropriate_package_found_create_namespace_and_load_package_contents; }

我想我已经证明 MLS 根本没有查找require,或者,如果它这样做了,那只有模块成功加载之后。

编译器首先遵循的步骤是什么,然后是运行时?

这个答案当然是试图回答这个问题,但我简要的编译器代码分析可能会有所帮助。3(虽然点击链接查看实际代码Actions.nqp并不适合胆小的人!)

[ 后记 ]

从这个意义上说,在 require “指令”之后的“动态查找结构”用于两件事:

  1. 通知编译器构造是“动态的”(所以不要在编译时修复任何东西)

  2. 提供将用于搜索符号、命名空间、文件或存储库内容的字符串

我认为它只做 2,只是一个传递给 MLS 的包名。

当我们加载同一个库时,“require”不会抛出任何异常。它是否默默地忽略了加载的库?

我认为require对此一无所知。它把它交给 MLS,然后在 MLS 完成它的事情后拿起它。我认为require无法区分 MLS 何时成功执行新加载和何时跳过加载。它所知道的只是 MLS 是说一切都好还是有例外。

当它可以首先检查同一个命名空间是否已经在使用时,为什么还要费心做这么多工作呢?

当 MLS 已经完成并且无论如何都要调用 MLS时,为什么还要费心去做任何工作呢?require任何事都是白费力气。

所要做require的就是处理用户在语句中明确输入的编译阶段符号。require不能要求 MLS 处理这些,因为它与成功的模块加载无关,这是 MLS 摆弄符号的唯一场景。

相反,当我们假设我们加载了一个不同的库时,它会抛出一个异常:正在使用的符号的“重复定义”。

尝试这个:

require ::('foo');
require ::('other');

unit module foo;现在,当您更改infoo.pm6other.pm6to时再试一次unit module bar;。您仍然会得到相同的异常,但符号将是bar. 怎么可能require知道bar?它不能。例外来自 MLS,并且该符号仅由 MLS 知道。

因此我得出结论,也许“需要”首先检查与提供的字符串同名的命名空间。

除非您将 MLS 视为 的一部分,否则require我相信您现在可以看到您的“也许”资格是明智的。:)

我偶然发现了一个奇怪的行为......在(3)中查找'foo :: A'没有找到任何东西!

我对此有一个解释。我并不是说它是对的,但是当我写下这句话时,这对我来说似乎并不太奇怪:

use语句加载foo.pm6包。它定义了一个包foo,其中包含一个类A和导出A。这会在导入词法范围内产生一个符号,该符号foo绑定到一个包,该包包含一个符号A。它还会在导入词法范围内产生另一个符号A.

require语句加载other.pm6包。它定义了一个包foo,其中包含一个类B和导出B。这导致foo将导入词法范围中的符号重新绑定到不同的包,即包含符号的新包B。它还会在导入词法范围内产生另一个符号B.

较早的A徘徊。(换句话说,P6M 符号合并过程不包括删除符号。)但是foo::A,在绑定到符号的包中查找的foo不再存在,因为绑定到foo符号的包现在是包中的other.pm6包,已经覆盖了foo.pm6包中的那个。

与此同时,还有一个奇怪的地方:

try require ::($name);
with $! {.say};             # No such symbol 'other' ...

我认为这反映了在成功加载模块进行require(失败)查找。

请注意,如果模块加载失败,则不会出现此消息;这似乎再次证实了我的想法(和代码2),即require在成功加载之前不进行任何查找(如果那样的话;我仍然没有强烈的感觉是 MLS 正在做这些事情还是require代码4对我来说太复杂了atm)。

对您的评论的回应

根据您对此答案的评论:

就像我们将 require +“动态查找公式”合并的结果一样,得到了像这样的增强动态查找{ ::('something') :if_not_found_as_namespace_check_repositories_and_load }

由于各种原因,这对我来说并不正确。

例如,假设有一个foo声明为module foo { our sub bar is export { say 99 } }如果required 将成功加载的包。现在考虑这段代码:

my \foo = 42;
say ::('foo');             # 42
require ::('foo') <&bar>;
say foo;                   # 42
bar;                       # 99

这对我来说很有意义。它不会加载名称为42. 它不会查找符号foo。它将加载名称为 的包foo。虽然它可能会在加载包foo 查找符号,但它不会安装符号foo,因为已经有一个。

脚注

1模块加载子系统是指给定模块名称的各个部分,它们执行诸如搜索本地文件系统或数据库、检查预编译目录、调用编译以及在模块成功加载时合并符号等操作。我不知道部件之间、部件和编译器之间的界限在哪里。但我相信它们不是它的一部分,require而只是被它调用。


2运行此代码:

my \MODULE =
  { my $v;
    Proxy.new:
      FETCH => method { say "get name: $v"; $v },
      STORE => method ($n) { say "set name: $n"; $v = $n }}();

MODULE = 'unseen by `require`';
say ::('MODULE');

use lib '.';
say 'about to `require`';
require ::('MODULE');

3我们从Raku文件中的相关匹配Grammar.nqp开始:

  rule statement_control:sym<require> {
        <sym>
        [
        | <module_name>
        | <file=.variable>
        | <!sigil> <file=.term>
        ]
        <EXPR>?
    }

代码似乎符合我们的预期——一个require关键字后跟:

  • 包标识符(<module_name>);或者

  • 一个<variable>(例如$foo);或者

  • a<term>不是以 a 开头的<sigil>

我们对<module_name>分支感兴趣。它调用token module_namewhich 调用token longnamewhich 调用token name

token name {
        [
        | <identifier> <morename>*
        | <morename>+
        ]
}

显然::('foo')不是以<identifier>. 所以它是token morename。我将删去一些无趣的行离开:

    token morename {
        '::'
        [
        ||  <?before '(' | <.alpha> >
            [
            | <identifier>
            | :dba('indirect name') '(' ~ ')' [ <.ws> <EXPR> ]
            ]
        ]?
    }

答对了。那将匹配::(,尤其是:dba('indirect name') '(' ~ ')' [ <.ws> <EXPR> ]位。

所以在这一点上,我们将捕捉到:

statement_control:sym<require><module_name><longname><name><morename><EXPR>

不久之后,statement_control:sym<require>令牌将要成功。所以到那时它会调用相应的动作方法Actions.nqp...


4Actions.nqp我们找到对应的动作token statement_control:sym<require>,即method statement_control:sym<require>。打开if $<module_name> {条件将是True,导致运行此代码:

$longname := $*W.dissect_longname($<module_name><longname>);
$target_package := $longname.name_past;

在我看来,这段代码正在剖析 parsing 的结果::('foo'),并将对应于该剖析的 AST 绑定到$target_package,而不是费心进行查找或准备运行时查找。

如果我是对的,::('foo')不需要超过 9 个字符require即可解释,但它喜欢解释它们。这里没有必要暗示它会做任何特定的事情,例如查找,因为它构造了包加载代码。


动作的后半部分确实会进行查找。有这样的

?? self.make_indirect_lookup($longname.components())

并且给定例程名称,我认为这在进行查找,require如果包加载成功,可能是尝试添加包符号的一部分。

于 2020-06-01T13:45:47.587 回答
2

require如果可以的话,在编译期间做一些事情。

require Module;
say Module;

它假定加载该模块将为您提供名称为Module.

因此它会在编译时安装一个具有该名称的临时符号。

这是它在编译时做的唯一事情。
(所以当我说“一些事情”时,我撒了谎。)

if Bool.pick {
    require module-which-does-not-exist;

    module-which-does-not-exist.method-call()
}

大约一半的时间上面什么都不做。
另一半时间它在运行时抱怨它找不到模块。

(我选择了Bool.pickFalse所以编译时优化器肯定无法优化它。)


当您使用标识符以外的其他内容调用它时,它在编译时不知道模块将是什么。所以它不能创建一个临时命名空间。

require 'Module';
say Module; # COMPILE ERROR: undeclared name
require Module; # RUNTIME ERROR: can't find 'Module'
say Module;
require 'Module'; # RUNTIME ERROR: can't find 'Module'
say ::('Module');
if False {
    require Module;
    say Module;
}
# no error at all
if False {
    require 'Module';
    say ::('Module');
}
# no error at all
于 2020-06-01T17:06:03.120 回答