【问题标题】:Understanding Raku's `&?BLOCK` compile-time variable了解 Raku 的 `&?BLOCK` 编译时变量
【发布时间】:2020-11-15 12:32:48
【问题描述】:

我非常感谢 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) ... }

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

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

最后,为什么有些块有OUTER::&lt;$_&gt; 而其他块没有?我对第 2 块感到特别困惑,它不是最外面的块。

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

【问题讨论】:

    标签: metaprogramming raku compile-time rakudo


    【解决方案1】:

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

    为什么&amp;?OUTER::BLOCK 引用(基于其 ID)阻止 2 而不是阻止 1?

    根据文档,&amp;?BLOCK 是一个“特殊的编译变量”,所有以? 为枝条的变量都是如此。

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

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

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

    正确使用 OUTER$a 会导致它引用外部范围

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

    OUTER 不能与&amp;?BLOCK 一起使用吗?

    我认为不是因为上述原因。看看有没有人纠正我。

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

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

    为什么后缀for 前面的&amp;?BLOCK 也引用它自己的块?

    直到你知道原因才会令人惊讶,但是......

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

    似乎是这样,所以主体可以接受for 的每次迭代传递的参数。

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

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

    这不正确吗?

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

    为什么有些块有OUTER::&lt;$_&gt;,而有些没有?

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

    为了支持这种可以传递或不传递参数的美妙习惯,除了自动输入$_ 的块之外,其他块都被赋予了将$_ 绑定到外块的$_

    我对第 2 块特别困惑,它不是最外面的块。

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

    【讨论】:

      【解决方案2】:

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

      只要看到特殊变量$?BLOCK,块对象就会存储在编译代码中。

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

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

      碰巧$?OUTER::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/闭包,以便for 可以将$_ 设置为当前值。 将其伪装成 Block 可能比仅为该用途创建新的 Code 类型更容易。
      特别是因为整个 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::&lt;$_&gt; ) 作为签名,它可以满足两种使用情况。

      这使得创建简单的 lambda 表达式变得很容易,只做你想让它们做的事情。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-02-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多