在Perl 6中,嵌套和非嵌套地图的上下文有什么区别?

6
我有这段代码。第一个非嵌套的 map 输出了一些内容,而嵌套的则没有。我认为我知道第二个为什么不工作了。它是一个惰性序列,Perl 6 正在收集结果。这很好。但是第一个(非嵌套)map 是否以相同的方式惰性呢?如果我不做任何处理,则它如何输出任何内容?也就是说,第一个是否惰性?它是否自动获得下沉上下文,我必须明确提供 sink(或其他内容)给嵌套的那一个?不知怎么回事,我觉得 Perl 6 应该能够为我解决这个问题。
my @array = (1, 2), (3, 4), ('a', 'b');

say "---On its own:";
my @item = 1, 2, 3;
@item.map: {
    say $_;
    };

say "---Inside another map:";
@array.map: {
    my @item = 1, 2, 3;
    @item.map: {
        say $_;
        }
    };

这里是输出结果:
---On its own:
1
2
3
---Inside another map:

这与问题“如何在Perl 6的for循环中使用map?”有关。那个答案告诉我们该怎么做,但我想问一个更深层次的问题——为什么要这样做。

在那个问题的解决方案中,需要在map内部添加eagersink或赋值操作:

say "---eager:";
@array.map: {
    my @item = 1, 2, 3;
    eager @item.map: {
        say $_;
        }
    };

say "---sink:";
@array.map: {
    my @item = 1, 2, 3;
    sink @item.map: {
        say $_;
        }
    };

say "---assignment:";
@array.map: {
    my @item = 1, 2, 3;
    @ = @item.map: {
        say $_;
        }
    };

不知怎么的,我认为Perl 6应该能够为我解决这个问题。返回Seq是一个有效的用例。由于编译器无法读取思想,您希望使用什么样的常规语义?将sink上下文递归地迭代序列的所有子序列作为sink上下文?对我来说,那看起来像是一罐蠕虫,可能应该保持关闭状态。 - Christoph
我更喜欢像地图一样返回列表的东西。它是一个序列而不是有序列表的想法对我来说似乎很奇怪。 - brian d foy
回顾过去,我也花了一些时间才适应序列:理论上,它们是更好的基元(例如,你可以轻松地将序列转换为列表,但反之则不行),但在实践中存在一些摩擦。然而,这个特定的问题与序列与列表的比较无关,而是与懒惰有关... - Christoph
1
所以你想让 map { $_+1 }, 1..* 或者 [\+] 1..* 返回一个列表?如果你只需要 Seq 的前几个元素,它只会产生前几个元素而不会浪费大量时间。say [\+](1,3...*)[^20] Seqs 在迭代使用后也可以丢弃值(如果你没有对它们进行缓存)。曾经有一段时间 List 承担了这两个任务,即使对于实现者来说,这也是非常难以理解的。 - Brad Gilbert
我并不是特别想要什么。更多的是初学者在使用某些东西之前需要了解多少知识。在这里,他们必须理解相当多的内容,因此会犯这些错误。 - brian d foy
2个回答

9
每个程序文件、块、子程序等的内容都是由分号分隔的语句列表组成。在此基础上:
  1. 被“沉没”的语句:

    • 除了语句列表中的最后一个语句外,其余所有语句。
    • 任何循环语句(如forwhile等)在语句列表级别上,即使它是最后一个语句也是如此。
    • 任何程序或模块文件的顶层语句列表中的语句,即使它是最后一个语句也是如此。
  2. 返回而不是沉没的语句:

    • 语句列表中的最后一个语句,除了上述情况。

沉没强制执行急切评估,返回则不会。

示例

在您的情况下,第一个map语句位于语句列表中间,因此它被“沉没”。

但是嵌套的map语句是其语句列表的最终语句,因此它的结果以未迭代的Seq形式返回。
其父map语句也是最终语句,但它在程序文件的顶层语句列表中,因此它被sunk,导致它急切地迭代由三个Seq值组成的序列。(在内部map之前插入一个say语句,以查看此内容。)
但是没有任何东西会sink或以其他方式迭代这三个内部Seq值。

来自设计文档

更详细地说,来自Synopsis 04, line 664:
在任何语句序列中,只有最后一个语句的值会被返回,因此所有之前的语句都在接收上下文中进行评估,这是自动渴望的,以强制执行副作用的评估。(副作用是执行此类语句的唯一原因,事实上,如果您执行“无用”的接收上下文操作,Perl将警告您。)接收上下文中的循环不仅会渴望地评估自己,而且可以优化掉循环中任何值的生成。
语句列表的最后一个语句不是接收上下文,可以返回任何值,包括惰性列表。但是,为了支持命令式程序员的期望(事实证明我们中的绝大多数人),任何显式循环作为语句列表的最后一个语句被自动强制使用接收上下文语义,以便循环在从块返回之前执行完毕。
这种强制的接收上下文仅适用于循环,而不是单个语句或半列表作为参数解析的结构。假定要求该语句的结果的结构仍然懒惰,即使该语句是一个循环。

1) 当一个闭合括号}出现在一行的最后一个正式标记时,就像这样
my @a = @b.map: { $_ + 1 } # 空格/注释不计入
它也结束了当前语句,但否则需要使用分号来分隔语句。

2) map不计入,因为它是一个函数而不是循环关键字。

3) 这意味着当一个循环语句出现在与语句列表直接不同的地方时,例如
lazy for ^10 { .say } # 作为期望单个语句的关键字的参数
(for ^10 { .say }) # 在表达式内部
那么默认情况下它不会被下沉。这就是概要引文的最后一段试图说的内容。

更新:在Rakudo中似乎并非如此,但这可能是一个错误

4) 这个规则在概要中没有提到,但它是Rakudo中的工作方式,我相信这是有意的。


有趣的是,没有任何警告。 - brian d foy

6

.map基本上返回一个.Seq。内部的map返回一个Seq给外部的map,但由于该map的结果被抑制了,它们在没有迭代的情况下消失了。

如果你使用外部的map,你将提取内部map的结果,并可以看到内部map返回的.Seq的结果:

my @array = (1, 2), (3, 4), ('a', 'b');
say "---Inside another map:";
say @array.map: {
    my @item = 1, 2, 3;
    @item.map: {
        say $_;
        }
    }
---Inside another map:
1
2
3
1
2
3
1
2
3
((True True True) (True True True) (True True True))

希望这样解释清楚了 :-)

另一种解决方法是在外部的 map 中加入一个特定的返回值。这样,内部的 map 就会被包含进去,然后就可以像下面这样迭代:

my @array = (1, 2), (3, 4), ('a', 'b');
say "---Inside another map:";
say @array.map: {
    my @item = 1, 2, 3;
    @item.map: {
        say $_;
        }
    42   # make sure ^^ map is sunk
    }
---Inside another map:
1
2
3
1
2
3
1
2
3

@briandfoy:你的意思是让垃圾回收器在释放既没有被sink也没有被迭代的Seq时发出警告吗? - smls
就解决方案而言,最好的建议是在命令式编程中使用循环关键字(如for)而不是函数(如map),这样你就不会遇到这样的意外了。 - smls
我不在乎是谁发出警告。即使是编译时警告也可以。 - brian d foy
@briandfoy:我认为编译器在一般情况下无法聪明到在编译时就能解决这个问题(例如,方法调用是动态解析的,因此它可能甚至还不知道.map返回一个Seq)。此外,请记住,您不会希望仅因为从块返回了尚未迭代的Seq而发出警告,因为当您以更“函数式编程”风格编写代码时,这通常是一种预期行为!您必须捕获那些可能是无意的情况。我不确定我的“垃圾收集器”想法是否会在不造成误报的情况下做到这一点。 - smls
1
总的来说,Perl 6支持几乎没有令人惊讶的“命令式编程”,包括for, while, 数组变量,赋值等。其他特性如map, X, Z, 绑定等则更适用于函数式编程,并需要理解语言中懒加载的处理方式。 - smls
“惊喜免费”并不是我的经验。这就像“可读性”一样,取决于你喜欢阅读什么(或者会让你感到惊讶的事情)。 - brian d foy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接