了解 Raku 的“&?BLOCK”编译时变量

Understanding Raku's `&?BLOCK` compile-time variable

我真的很欣赏 Raku 的 &?BLOCK 变量——它可以让你在一个未命名的块中递归,这 非常 强大。例如,这是一个简单的、内联的、匿名的阶乘函数:

{ when $_ ≤ 1 { 1 }; 
  $_ × &?BLOCK($_ - 1) }(5) # OUTPUT: «120»

但是,在更复杂的情况下使用时,我对此有一些疑问。考虑这段代码:

{   say "Part 1:";
    my $a = 1;
    print '    var one: '; dd $a;
    print '  block one: '; dd &?BLOCK ;
    {
        my $a = 2;
        print '    var two: '; dd $a;
        print '  outer var: '; dd $OUTER::a;

        print '  block two: '; dd &?BLOCK;
        print "outer block: "; dd &?OUTER::BLOCK
    }
    say "\nPart 2:";
    print '  block one: '; dd &?BLOCK;
    print 'postfix for: '; dd &?BLOCK for (1);
    print ' prefix for: '; for (1) { dd &?BLOCK }
};

产生此输出(我缩短了块 ID):

Part 1:
    var one: Int $a = 1
  block one: -> ;; $_? is raw = OUTER::<$_> { #`(Block|…6696) ... }
    var two: Int $a = 2
  outer var: Int $a = 1
  block two: -> ;; $_? is raw = OUTER::<$_> { #`(Block|…8496) ... }
outer block: -> ;; $_? is raw = OUTER::<$_> { #`(Block|…8496) ... }

Part 2:
  block one: -> ;; $_? is raw = OUTER::<$_> { #`(Block|…6696) ... }
postfix for: -> ;; $_ is raw { #`(Block|…9000) ... }
 prefix for: -> ;; $_ is raw { #`(Block|…9360) ... }

以下是我不明白的地方:为什么 &?OUTER::BLOCK 引用(基于其 ID)阻止二而不是阻止一?将 OUTER$a 一起正确使用会导致它引用外部范围,但同样的事情不适用于 &?BLOCKOUTER&?BLOCK 一起使用是不可能的吗?如果没有,有没有办法从内部块访问外部块? (我知道我可以将 &?BLOCK 分配给外部块中的命名变量,然后在内部块中访问该变量。我认为这是一种解决方法,但不是完整的解决方案,因为它牺牲了引用未命名的能力块,这是 &?BLOCK 的大部分能量来源。)

其次,我对第2部分很困惑。我理解为什么前缀for后面的&?BLOCK指的是一个内部块。但是为什么 后缀之前的 &?BLOCK 引用它自己的块?是否在 for 语句的主体周围隐式创建了一个块?我的理解是后缀形式在很大程度上是有用的,因为它们需要块。这不正确吗?

最后,为什么有的块有OUTER::<$_>,有的却没有?我对第 2 块感到特别困惑,它 不是 最外面的块。

提前感谢您提供的任何帮助! (如果上面显示的任何代码行为表明存在 Rakudo 错误,我很乐意将其写为问题。)

这是您遇到的一些非常令人困惑的事情。也就是说,它确实有某种意义......

Why does the &?OUTER::BLOCK refer (based on its ID) to block two rather than block one?

根据文档,&?BLOCK 是一个“特殊的 compile 变量”,所有以 ? 为分支的变量都是如此.

因此,它不是一个可以在 运行-time 中查找的符号,这就是像 $FOO::bar 这样的语法应该是关于阿法克

所以我认为编译器应该有权拒绝在包查找语法中使用“编译变量”。 (虽然我不确定。在 COMPILING 包中进行“运行 次”查找是否有意义?)

可能已经有一个错误归档(在 GH 存储库 rakudo/rakudo/issues 或 raku/old-issues-tracker/issues 中)关于尝试对 运行-time 查找是错误的特殊编译变量(带有 ? twigil 的变量)。如果没有,我可以提交一份。

Using OUTER with $a correctly causes it to refer to the outer scope

与外部块中的$a变量关联的符号存储在与外部块关联的stash中。这是 OUTER.

引用的内容

Is it just not possible to use OUTER with &?BLOCK?

我认为不是因为上面给出的原因。看看有没有人指正。

If not, is there a way to access the outer block from the inner block?

您可以将其作为参数传递。换句话说,用 }(&?BLOCK); 而不是 } 关闭内部块。然后你可以在内部块中以 $_ 的形式使用它。

Why does the &?BLOCK that precedes the postfix for also refer to its own block?

直到你知道为什么,这才令人惊讶,但是......

Is a block implicitly created around the body of the for statement?

似乎是这样,所以正文可以接受 for.

的每次迭代传递的参数

My understanding is that the postfix forms were useful in large part because they do not require blocks.

我一直认为它们的好处是 A) 避免单独的词法作用域和 B) 避免必须输入大括号。

Is that incorrect?

好像是。 for 必须能够为其语句提供不同的 $_ (您可以将一系列语句放在括号中),因此如果您不显式编写大括号,它仍然必须创建一个独特的词汇框架,并且可能认为 &?BLOCK 变量用它自己的 $_ 跟踪那个不同的框架并“假装”这是一个“块”,并用{...},尽管没有明确的 {...}

Why do some of the blocks have OUTER::<$_> in them but others do not?

虽然 for(和 given 等)总是将“它”又名 $_ 参数传递给它的 blocks/statements,但其他块 不会 有一个参数 自动 传递给他们,但是 他们将接受一个 如果它是由代码编写者手动传递的.

为了支持可以传递或不传递参数的这种奇妙的习惯用法,除了自动提供 $_ 的块之外,还提供了此 默认值 $_ 绑定到外部块的 $_.

I'm especially confused by Block 2, which is not the outermost block.

我很困惑你对此特别困惑。 :) 如果上述内容没有为您充分阐明最后一个方面,请评论最后一点特别令人困惑的地方。

在编译期间,编译器必须跟踪各种事情。其中之一是它正在编译的当前块。

块对象在它看到特殊变量的任何地方都会存储在编译代码中 $?BLOCK

基本上编译时变量并不是真正的变量,更像是一个宏。

因此,只要它看到 $?BLOCK,编译器就会将其替换为编译器当前正在编译的任何当前块。

碰巧 $?OUTER::BLOCK 在某种程度上与 $?BLOCK 足够接近,它也取代了 $?BLOCK

我可以通过尝试按名称查找变量来向您证明确实没有该名称的变量。

{ say ::('&?BLOCK') } # ERROR: No such symbol '&?BLOCK'

每对 {}(不是哈希引用或哈希索引)也表示一个新块。

所以每一行都会表达不同的意思:

{
  say $?BLOCK.WHICH;
  say "{ $?BLOCK.WHICH }";
  if True { say $?BLOCK.WHICH }
}

这意味着如果您在其中一个结构中声明一个变量,它就会包含在该结构中。

"{ my $a = "abc"; say $a }"; # abc
say $a; # COMPILE ERROR: Variable '$a' is not declared

if True { my $b = "def"; say $b } # def
say $b; # COMPILE ERROR: Variable '$b' is not declared

在后缀for的情况下,左边需要是一个lambda/closure以便for可以设置$_为当前值。 将其伪造成块可能比仅为该用途创建新的代码类型更容易。
特别是因为整个 Raku 源文件也被视为一个块。


裸块可以有一个可选参数。

my &foo;

given 5 {
  &foo = { say $_ }
}

foo(  ); # 5
foo(42); # 42

如果您给它一个参数,它会将 $_ 设置为该值。
如果不这样做,$_ 将指向任何 $_ 在该声明之外的内容。 (关闭)

对于该构造的许多用途,这样做非常方便。

sub call-it-a (&c){
  c()
}
sub call-it-b (&c, $arg){
  c( $arg * 10 )
}

for ^5 {
  call-it-a( { say $_ }     ); # 0␤ 1␤ 2␤ 3␤ 4␤
  call-it-b( { say $_ }, $_ ); # 0␤10␤20␤30␤40␤
}

对于 call-it-a,我们需要它作为 $_ 的闭包才能工作。
对于 call-it-b,我们需要它作为参数。

通过将 :( ;; $_? is raw = OUTER::<$_> ) 作为签名,它可以满足两种用例。

这使得创建简单的 lambda 变得容易,这些 lambda 只做您想让它们做的事。