在 Raku 中获取惰性序列的最后一个元素

Getting the last element of a lazy Seq in Raku

我想在 Raku 中获取一个惰性但有限的 Seq 的最后一个元素,例如:

my $s = lazy gather for ^10 { take $_ };

以下无效:

say $s[* - 1];
say $s.tail;

这些有效但似乎不太惯用:

say (for $s<> { $_ }).tail;
say (for $s<> { $_ })[* - 1];

在保持原始 Seq 惰性的同时执行此操作的最惯用方法是什么?

您所问的(“获取[ing]惰性但有限的 Seq 的最后一个元素……同时保持原始 Seq 惰性”)是不可能的。我并不是说 Raku 不可能 – 我的意思是,原则上,任何语言都不可能像 Raku 那样定义“惰性”,例如, is-lazy 方法。

如果特别说明,when a Seq is lazy in Raku,那“意味着 [Seq 的] 值是按需计算并存储以备后用。”此外,惰性迭代的定义特征之一是它无法知道自己的长度,同时保持惰性——这就是为什么在惰性迭代上调用.elems会抛出错误:

my $s = lazy gather for ^10 { take $_ };
say $s.is-lazy; # OUTPUT: «True»
$s.elems;       # THROWS: «Cannot .elems a lazy list onto a Seq»

现在,在这一点上,您可能会合理地想“好吧,也许 Raku 不知道 $s 有多长,但是 可以看出它正好有 10 个元素。"你没看错——使用该代码,$s 确实保证有 10 个元素。这意味着,如果您想获得 $s 的第十个(最后一个)元素,您可以使用 $s[9] 来实现。像这样访问 $s 的第十个元素不会改变 $s.is-lazy.

的事实

但是,重要的是,您只能这样做,因为您知道一些关于 $s 的“额外”信息,并且这些额外信息消除了您可能希望列表在实践中变得惰性的大部分原因。

为了理解我的意思,考虑一个非常相似的 Seq

my $s2 = lazy gather for ^10 { last if rand > .95; take $_ };
say $s2.is-lazy; # OUTPUT: «True»

现在,$s2可能 有 10 个元素,但也可能不是——唯一知道的方法是遍历它并找出答案。反过来,这意味着 $s2[9] 不会像 $s[9] 那样 跳到第十个元素;它会像您需要的那样遍历 $s2。因此,如果你 运行 $s2[9],那么 $s2 将不再懒惰(即 $s2.is-lazy 将 return False) .

实际上,这就是您在问题代码中所做的:

my $s = lazy gather for ^10 { take $_ };
say $s.is-lazy;              # OUTPUT: «True»
say (for $s<> { $_ }).tail;  # OUTPUT: «9»
say $s.is-lazy;              # OUTPUT: «False»

因为 Raku 永远不知道它已经到达惰性 Seq 的 tail,所以它可以告诉您 .tail 的唯一方法是完全迭代 $s。这必然意味着 $s 不再懒惰。

两个并发症

值得一提的是两个相邻的主题,它们实际上并不相关,但它们非常接近,以至于它们会误导一些人。

首先,我所说的关于不知道其长度的惰性迭代器并没有排除一些-惰性迭代器不知道它们的长度。事实上,相当多的 Raku 类型都做到了 Iterator role and the PredictiveIterator role – and the main point of a PredictiveIterator is that it does know how many elements it can produce without needing to produce/iterate them. But PredictiveIterators cannot be lazy.

第二个可能令人困惑的主题与第一个密切相关:虽然没有 PredictiveIterator 可以是惰性的(也就是说,none 将永远有一个 .is-lazy 方法 returns True),一些 PredictiveIterators 的行为与懒惰非常相似——事实上,甚至可以通俗地称为“懒惰”。

我无法很好地解释这种区别,因为老实说,我自己也不完全理解。但我可以举个例子:IO::Handle 上的 .lines 方法。毫无疑问,读取一个巨大文件的行的行为很像处理一个惰性可迭代对象。最明显的是,您可以处理每一行,而无需将整个文件放在内存中。文档甚至说使用 .lines 方法“懒惰地读取行”。

另一方面:

my $l = 'some-file-with-100_000-lines.txt'.IO.lines;
say $l.is-lazy;                         # OUTPUT: «False»
say $l.iterator ~~ PredictiveIterator;  # OUTPUT: «True»
say $l.elems;                           # OUTPUT: «100000»

所以我不太确定说 $l“是一个惰性可迭代对象”是否公平,但如果是的话,它的“惰性”方式与 $s 不同.

我知道很多,但希望对您有所帮助。如果您有一个更具体的懒惰用例(我打赌它不是从零到九收集数字!),我很乐意更具体地解决这个问题。如果其他人可以用 .lines 和其他不懒惰的 PredictiveIterator 填写一些细节,我将不胜感激!

删除 lazy

Raku 中的惰性序列旨在按原样正常工作。您不需要通过添加显式 lazy.

来强调 他们很懒惰

如果添加 explicit lazy,Raku 会将其解释为阻止 .tail 等操作的请求,因为它们几乎肯定会立即呈现惰性没有实际意义,并且,如果在无限序列上调用,甚至只是一个足够大的序列,挂起或 OOM 程序。

因此,要么删除 lazy,要么不要调用 .tail 之类的操作,否则会被阻止。

我的原始答案的扩展版本

如@ugexe 所述,惯用的解决方案是删除 lazy.

引用 :

if a gather is asked if it's lazy, it returns False.

Aiui,适用于以下情况:

  • 一些惰性序列生产者实际上或实际上是无限的。如果是这样,对它们调用 .tail 等将挂起调用程序。相反,其他惰性序列在一次消耗完所有值时表现良好。 Raku应该如何区分这两种情况?

  • 2015 年决定让价值产生数据类型强调或de通过他们对 .is-lazy 电话的回应来强调他们的懒惰。

  • 返回 True 表明一个序列不仅是懒惰的,而且想要 已知 通过使用调用 .is-lazy. (没有那么多最终用户代码,而是内置了消费功能,例如 @ 符号变量处理分配,试图确定是否分配 eagerly。)内置消费功能将 True 作为信号,它们应该阻止像 .tail 这样的调用。如果开发人员知道这过于保守,他们可以添加 eager(或删除不需要的 lazy)。

  • 相反,一个数据类型,甚至一个特定的对象实例,可能 return False 表示它 想要被认为是懒惰的。这可能是因为特定数据类型或实例的实际行为是急切的,但它可能 惰性 技术上,但不是不希望消费者阻止诸如 .tail 之类的操作,因为它知道它们不会有害,或者至少更愿意将其作为默认假设。如果开发人员知道得更多(因为,比如说,它挂起程序),或者至少 不想 想要阻止可能有问题的操作,他们可以添加 lazy(或删除不需要的 eager).

我认为这种方法效果很好,但提到“懒惰”的文档和错误消息可能没有赶上 2015 年的转变。所以:

进一步讨论

the docs ... say “If you want to force lazy evaluation use the lazy subroutine or method. Binding to a scalar or sigilless container will also force laziness.”

是的。 Aiui 没错。

[which] sounds like it implies “my $x := lazy gather { ... } is the same as my $x := gather { ... }”.

没有

一个显式的 lazy 语句前缀或方法增加了 强调 懒惰,Raku 将其解释为意味着它应该阻止像 .tail 这样的操作,以防它们挂程序。

相比之下,绑定到一个变量既不会改变强调也不会强调懒惰,只是转发任何绑定的生产者datatype/instance 已选择通过 .is-lazy.

传送

not only in connection with gather but elsewhere as well

是的。这是关于 .is-lazy:

的结果
my $x =      (1, { .say; $_ + 1 } ... 1000);
my $y = lazy (1, { .say; $_ + 1 } ... 1000);

both act lazily ... but $x.tail is possible while $y.tail is not.

是的。

显式 lazy 语句前缀或方法强制 .is-lazy 的答案为 True。这向关心懒惰危险的消费者发出信号,表明它应该变得谨慎(例如拒绝 .tail 等)。

(相反,eager 语句前缀或方法可用于强制 .is-lazy 的答案为 False,使胆小的消费者接受 .tail 等调用.)

I take from this that there are two kinds of laziness in Raku, and one has to be careful to see which one is being used where.

两种我称之为消费指导:

  • 不要尾随我 如果对象 returns True 来自 .is-lazy 调用则它被视为可能是无限的。因此像.tail这样的操作被阻止了。

  • You-can-tail-me 如果对象 returns False 来自 .is-lazy 调用则接受像 .tail 这样的操作。

并不是需要小心这两种类型中的哪一种在起作用,但是如果想要调用像tail这样的操作,那么可能需要通过插入 eager 或删除 lazy 来启用它,并且必须对后果负责:

  • 如果程序由于使用 .tail 而挂起,好吧,DIHWIDT.

  • 如果你突然消耗了一个惰性序列的所有内容并且没有缓存它,那么,也许你应该缓存它。

我想说的是错误消息 and/or 文档可能需要改进。