【问题标题】:Behaviour of require (static + dynamic) [ RAKU ]require 的行为(静态 + 动态)[ RAKU ]
【发布时间】:2020-06-01 11:48:24
【问题描述】:

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

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

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

    { require MODULE; }

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

{
    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”的行为 NOT 作为“正常”子例程。

当我们将“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)它会引发异常。

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

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

如果我们使用“use 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

这是我遗漏的错误还是其他什么?

【问题讨论】:

  • 我没有读过你的问题,但你读过stackoverflow.com/questions/56089611/…吗?你的问题本质上是重复的吗?
  • 是的,我有!但是我的问题与使用一种或另一种方式所产生的影响无关(至少乍一看不是)。我了解两种方式(静态和动态)的使用。它更多地是关于“if”和“how”,“require”语句不是一个普通的子程序,而是动态查找的转换代理。这只是确认我对事物的理解的问题。
  • 好的。明白了。 :) 但是您在问题中的示例仍然是错误的。当我解决问题时,我做的第一件事就是加载示例代码并运行它。如果它没有按照提问者所说的那样做,那就令人困惑了。您的代码反映了您的困惑。如果您写了{ my $foo = ::('EXAMPLE'); try require EXAMPLE },您可能会对此有所了解。同时,请考虑修改您的示例代码,或者更确切地说是 cmets,以对读者有意义。 TIA。
  • "虽然require的执行顺序在查找之后"。这是不正确的。代码的执行顺序不一定遵循人类的阅读顺序。一般来说,Raku 中的所有代码都受到两个阶段和相移的影响。作为一个具体点,require 是一个语句,它的一些行为在整个编译阶段执行,首先出现,一些在整体运行阶段执行,后面出现。 require 的编译阶段方面的执行发生在 查找的运行阶段方面的执行之前。
  • 如果您编写了BEGIN { my $response = ::('MODULE'); say $response.^name },那么您将相移大括号中的代码以在编译阶段执行,它会在之前执行@的编译阶段方面987654338@,所以它会显示Failure。 “模块不存在,因此查找导致编译阶段占位符包:模块”我认为这反映了您的困惑。 MODULE 确实 存在于您的示例代码中。您的say...require MODULE 执行其编译阶段行为之后执行,其中一部分是声明MODULE 符号。

标签: require lookup raku rakudo


【解决方案1】:

重写以对应您答案的the third version

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

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

更具体一点,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 &lt;A&gt;; --> MODULEA

  • require 'MODULE.pm6' &lt;A&gt;; --> A.

  • require ::('MODULE') &lt;A&gt;; --> A.

Aiui MLS1,作为符号merging (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

如果您在 the official doc 中搜索 require,您会看到它没有列在 常规参考 部分,而是列在 的模块部分语言参考。它是一个关键字、一个语句,是编译器能够理解的语言的一个特殊部分。

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

动态查找的结果是绑定到Symbol(如果已声明)的值,否则为Failure

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

$variable 不是命名空间。

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

鉴于我编写的跟踪动态查找 ::('MODULE')2 值的代码,在我看来,无论是@987654357,任何代码都没有动态查找它@ 或 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; }

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

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

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

[后记]

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

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

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

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

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

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

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

既然 MLS 已经完成了 any 工作,而且require 无论如何都会调用 MLS,为什么还要麻烦呢?做任何事情都是白费力气。

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

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

试试这个:

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

现在,将foo.pm6other.pm6 中的unit module foo; 更改为unit module bar;,再试一次。您仍然会遇到相同的异常,但符号将是 barrequire 怎么知道bar?它不能。例外来自 MLS,而符号只有 MLS 知道。

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

除非您将 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 符号的包中查找的 foo::A 不再存在,因为绑定到foo 符号的包现在是来自other.pm6 包的包,已覆盖来自foo.pm6 包的包。

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

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

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

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

对您的 cmets 的回应

来自您的 cmets 对此答案:

就像我们通过 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我们以the relevant match in Raku's Grammar.nqp file开头:

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

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

  • 一个包标识符(&lt;module_name&gt;);或

  • &lt;variable&gt;(例如$foo);或

  • &lt;term&gt; 不以 &lt;sigil&gt; 开头。

我们对&lt;module_name&gt; 分支感兴趣。它调用token module_name 调用token longname 调用token name

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

显然::('foo') 不以&lt;identifier&gt; 开头。所以它是token morename。我会删掉一些无趣的行离开:

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

宾果游戏。这将匹配::(,尤其是:dba('indirect name') '(' ~ ')' [ &lt;.ws&gt; &lt;EXPR&gt; ] 位。

所以此时我们将捕获:

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

不久之后,statement_control:sym&lt;require&gt; 令牌即将成功。所以此时它会调用Actions.nqp中对应的action方法...


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

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

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

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


动作的后半部分做查找。有this这样的行:

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

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

【讨论】:

  • @jakar 我已经添加了你的最后几段和我的回复。 PLMK(如果您同意),或者指定您使用的 Rakudo 版本(如果不同意)。谢谢。
  • 如果您有耐心,请重新阅读我的答案(我已对其进行了大量编辑)并评论任何错误或令人困惑的地方。也许我们可以将事情缩小到少数剩余的令人困惑的位。然后也许您可以编辑您的问题和/或我可以(至少暂时)删除此答案,和/或提供奖金以尝试获得一个好的答案。
  • 导入的符号也需要在编译时声明。
  • 嘿。我已经修改了我的整个答案。我建议您查看上面引用您的评论并提供代码示例的最后一点。然后考虑查看我所做的编译器代码探索。与此同时,我会去阅读你的问题修订版。
  • @jjmerelo 谢谢。我已将其添加到我的答案中。
【解决方案2】:

require 会在编译期间做一些事情。

require Module;
say Module;

它假设加载该模块会给你一些名为Module的东西。

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

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

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

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

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

(我选择了Bool.pick 而不是False,所以编译时优化器肯定无法将其优化掉。)


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

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

【讨论】:

    猜你喜欢
    • 2015-10-14
    • 2012-05-14
    • 1970-01-01
    • 1970-01-01
    • 2015-03-06
    • 2010-11-14
    • 1970-01-01
    • 2013-05-14
    • 1970-01-01
    相关资源
    最近更新 更多