理解 Raku 的 `&?BLOCK` 编译时变量

cod*_*ons 11 metaprogramming rakudo compile-time raku

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

{ when $_ ? 1 { 1 }; 
  $_ × &?BLOCK($_ - 1) }(5) # OUTPUT: «120»
Run Code Online (Sandbox Code Playgroud)

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

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

产生这个输出(我已经缩短了块 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) ... }
Run Code Online (Sandbox Code Playgroud)

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

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

最后,为什么有些块OUTER::<$_>在 中而其他块没有?我对 Block 2 尤其困惑,它不是最外面的块。

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

rai*_*iph 6

这是您遇到的一些非常令人困惑的事情。就是说,这一切都有些道理……

为什么&?OUTER::BLOCK引用(基于其 ID)阻止两个而不是阻止一个?

根据文档,&?BLOCK是一个“特殊的编译变量”,就像所有将 a?作为它们的 twigil 的变量一样。

因此,它不是一个可以在run-time 中查找的符号,这$FOO::bar应该是关于 afaik 的语法。

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

可能已经存在一个错误(在 GH repos rakudo/rakudo/issues 或 raku/old-issues-tracker/issues 中)关于尝试对特殊编译变量进行运行时查找是错误的(那些有?树枝的)。如果没有,提交一份对我来说是有意义的。

正确使用OUTERwith$a会导致它引用外部作用域

$a外部块中的变量关联的符号存储在与外部块关联的存储中。这就是引用的内容OUTER

只是不能OUTER与一起使用&?BLOCK吗?

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

如果没有,有没有办法从内部块访问外部块?

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

为什么&?BLOCK后缀之前的for也指的是它自己的块?

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

是否在for语句主体周围隐式创建了一个块?

看起来如此,所以主体可以接受 . 的每次迭代传递的参数for

我的理解是后缀形式在很大程度上很有用,因为它们不需要块。

我一直认为他们的好处是他们 A) 避免了单独的词法范围和 B) 避免必须输入大括号。

那不正确吗?

似乎是这样。for必须能够$_为其语句提供一个不同的(您可以在括号中放置一系列语句),因此如果您不明确地编写大括号,它仍然必须创建一个不同的词法框架,并且大概是考虑更好的&?BLOCK变量跟踪与自己那独特的框架$_,这是一个“块”,“假装”,并与所显示的,其要旨{...},尽管那里是没有明确{...}

为什么有些块中有OUTER::<$_>而其他块没有?

虽然forgiven等)总是将“it”又名$_参数传递给它的块/语句,但其他块没有自动传递给它们的参数,但如果代码编写者手动传递一个参数,它们将接受一个。

为了支持这一奇妙成语,其中一个可以通过或不通过一个参数,比那些其它块被自动送入一个$_被给予此默认结合$_到外块的$_

我对 Block 2 尤其困惑,它不是最外面的块。

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


Bra*_*ert 6

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

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

基本上编译时变量并不是真正的变量,而是更多的宏。

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

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

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

{ say ::('&?BLOCK') } # ERROR: No such symbol '&?BLOCK'
Run Code Online (Sandbox Code Playgroud)

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

所以每一行都会说一些不同的东西:

{
  say $?BLOCK.WHICH;
  say "{ $?BLOCK.WHICH }";
  if True { say $?BLOCK.WHICH }
}
Run Code Online (Sandbox Code Playgroud)

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

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

在 postfix 的情况下for,左侧需要是一个 lambda/closure,以便for可以设置$_为当前值。将它伪装成一个 Block 可能比创建一个专门用于该用途的新 Code 类型更容易。
特别是因为整个 Raku 源文件也被认为是一个块。


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

my &foo;

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

foo(  ); # 5
foo(42); # 42
Run Code Online (Sandbox Code Playgroud)

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

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

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

因为call-it-a我们需要它作为结束$_工作。
因为call-it-b我们需要它成为一个论点。

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

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