require 的行为(静态 + 动态)[RAKU]

jak*_*kar 6 lookup require rakudo raku

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

我将尝试介绍我对事物的理解:

[ 1 ] 使用带有文字的“require”

    { require MODULE; }
Run Code Online (Sandbox Code Playgroud)

在这种情况下,编译器会检查 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
}
Run Code Online (Sandbox Code Playgroud)

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

    { try require 'FILE_PATH'; }
Run Code Online (Sandbox Code Playgroud)

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

[ 3 ] 使用“require”进行动态查找

    { try require ::('MODULE'); }
Run Code Online (Sandbox Code Playgroud)

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

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

事实上,“动态查找”例程的结果要么是一个符号,要么是一个失败。

如果“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; }
Run Code Online (Sandbox Code Playgroud)

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

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

[ 后脚本 ]

我同意@raiph 的观点,即“require”不是一个子程序,它已经深深地集成到了语言中。

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

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

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

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

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

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

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

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

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

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

class A is export {}
Run Code Online (Sandbox Code Playgroud)

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

file "other.pm6" contents:
-----------------------------------
module foo {
    class B is export {}
}
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

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

顺便说一下,在检查这一点时,我偶然发现了一种奇怪的行为。这是以下内容:

如果我们使用“use foo;” 在线:“初始包加载”而不是“require ::('foo') ;”,我们得到以下结果:

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

(3) Any
(4) foo::A
Run Code Online (Sandbox Code Playgroud)

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

此外,如果我更改库文件: "other.pm6" 与以下(类 A 而不是 B - 如在 foo.pm6 中)

file "other.pm6" contents:
-----------------------------------
module foo {
    class A is export {}
}
Run Code Online (Sandbox Code Playgroud)

结果似乎恢复到预期:

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

(3) foo::A
(4) foo::A
Run Code Online (Sandbox Code Playgroud)

这是一个错误还是我遗漏的其他东西?

rai*_*iph 4

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

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

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

更具体地说,require 关键字及其生成的代码4完成了这项工作。

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

require FOO;
my FOO $bar; # Type 'FOO' is not declared
Run Code Online (Sandbox Code Playgroud)

# 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'); }
Run Code Online (Sandbox Code Playgroud)

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

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

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

say require MODULE;   # Undeclared name:
                            MODULE used at line 1
                      # Undeclared routine:
                            require used at line 1
Run Code Online (Sandbox Code Playgroud)

require如果您在官方文档中搜索,您会发现它没有列在例程参考部分中,而是列在语言参考的模块部分中。它是一个关键字、一条语句、编译器可以理解的语言的特殊部分。

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

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

my $variable = 42;
say ::('$variable');           # 42
say ::('nonsense') ~~ Failure; # True
Run Code Online (Sandbox Code Playgroud)

$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时,为什么还要费力去做任何工作呢?require任何事都是白费力气。

require要做的就是处理用户在语句中显式键入的编译阶段require符号。它不能要求 MLS 处理这些问题,因为它与成功的模块加载无关,而且这是 MLS 摆弄符号的唯一情况。

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

尝试这个:

require ::('foo');
require ::('other');
Run Code Online (Sandbox Code Playgroud)

unit module foo;现在,当您将infoo.pm6other.pm6更改为 时,再试一次unit module bar;。您仍然会得到相同的异常,但符号将为bar。怎样才能require知道呢bar?不可以。例外情况来自 MLS,并且只有 MLS 知道该符号。

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

除非你将美国职业足球大联盟视为 的一部分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' ...
Run Code Online (Sandbox Code Playgroud)

我认为这反映了在成功加载模块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 } }如果 d. 则将成功加载require。现在考虑这段代码:

my \foo = 42;
say ::('foo');             # 42
require ::('foo') <&bar>;
say foo;                   # 42
bar;                       # 99
Run Code Online (Sandbox Code Playgroud)

这对我来说很有意义。它不会加载名称为 的包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');
Run Code Online (Sandbox Code Playgroud)

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

  rule statement_control:sym<require> {
        <sym>
        [
        | <module_name>
        | <file=.variable>
        | <!sigil> <file=.term>
        ]
        <EXPR>?
    }
Run Code Online (Sandbox Code Playgroud)

该代码似乎遵循我们的预期——require关键字后跟:

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

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

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

我们对这个<module_name>分行很感兴趣。它调用token module_name哪个调用token longname哪个调用token name

token name {
        [
        | <identifier> <morename>*
        | <morename>+
        ]
}
Run Code Online (Sandbox Code Playgroud)

显然::('foo')不是以 开头<identifier>。所以就是token morename。我会删掉几行无趣的内容:

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

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

所以此时我们将捕获:

statement_control:sym<require><module_name><longname><name><morename><EXPR>
Run Code Online (Sandbox Code Playgroud)

不久之后,statement_control:sym<require>令牌即将成功。那么此时它将调用Actions.nqp...中相应的操作方法


4在 中Actions.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;
Run Code Online (Sandbox Code Playgroud)

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

如果我是对的,那么无论它想要如何解释它们,::('foo')都不需要超过 9 个字符。require这里没有必要暗示它会执行任何特定的操作,例如查找,因为它构造了包加载代码。


该操作的后半部分确实进行了查找。有这样的行:

?? self.make_indirect_lookup($longname.components())
Run Code Online (Sandbox Code Playgroud)

并给定例程名称,我认为正在执行查找,也许作为require在包加载成功时尝试添加包符号的一部分。