在 Raku 中获取惰性 Seq 的最后一个元素
Posted
技术标签:
【中文标题】在 Raku 中获取惰性 Seq 的最后一个元素【英文标题】:Getting the last element of a lazy Seq in Raku 【发布时间】:2022-01-10 15:11:33 【问题描述】:我想在 Raku 中获取一个惰性但有限的 Seq 的最后一个元素,例如:
my $s = lazy gather for ^10 take $_ ;
以下不起作用:
say $s[* - 1];
say $s.tail;
这些有效,但似乎不太习惯:
say (for $s<> $_ ).tail;
say (for $s<> $_ )[* - 1];
在保持原始 Seq 惰性的同时,最惯用的方法是什么?
【问题讨论】:
你不需要lazy
:my $s = gather for ^10 take $_; ; say $s.tail
我有点困惑。文档说“绑定到标量或无符号容器也会导致惰性”。如果我省略 lazy
关键字,为什么行为会有所不同?
@NikolaBenes 如果gather
被询问是否懒惰,则返回False
,而如果lazy
被询问是否懒惰,则返回@ 987654331@。请参阅我对About Laziness 的回复。
【参考方案1】:
删除lazy
Raku 中的惰性序列旨在按原样正常运行。您无需通过添加明确的lazy
来强调他们很懒惰。
如果您添加 显式 lazy
,Raku 会将其解释为阻止诸如 .tail
之类的操作的请求,因为它们几乎肯定会立即使惰性变得毫无意义,并且如果在无限的情况下调用序列,甚至只是一个足够大的序列,挂起或OOM程序。
所以,要么删除lazy
,要么不要调用.tail
之类的操作,否则会被阻止。
我的原始答案的扩展版本
正如@ugexe 所述,惯用的解决方案是删除lazy
。
引用my answer to the SO About Laziness:
如果
gather
被询问,如果它是惰性的,它会返回False
。
Aiui,类似以下内容:
一些惰性序列生产者可能实际上或实际上是无限的。如果是这样,对它们调用.tail
等将挂起调用程序。相反,其他惰性序列在一次性消耗完所有值时表现良好。 Raku 应该如何区分这两种情况?
2015 年做出了一项决定,让产生价值的数据类型通过对.is-lazy
调用的响应来强调或de强调他们的懒惰。
返回True
表示序列不仅是惰性的,而且希望通过使用调用.is-lazy
的代码知道惰性。 (与其说是最终用户代码,不如说是内置消费功能,例如 @
标记变量,处理分配试图确定是否急切。)内置消费功能采用@987654343 @ 作为一个信号,他们应该阻止像 .tail
这样的调用。如果开发人员知道这过于保守,他们可以添加eager
(或删除不需要的lazy
)。
相反,一个数据类型,甚至是一个特定的对象实例,可能会返回False
表示它确实不想被视为惰性。这可能是因为特定数据类型或实例的实际行为是急切的,但也可能是它在技术上是懒惰的技术,但不希望消费者阻止诸如.tail
之类的操作,因为它知道它们不会有害,或者至少更愿意将其作为默认假设。如果开发人员知道得更好(例如,因为它挂起程序),或者至少不想要阻止可能有问题的操作,他们可以添加lazy
(或删除不需要的eager
)。
我认为这种方法效果很好,但它的文档和错误消息提到“懒惰”可能没有赶上 2015 年的转变。所以:
如果您对一些关于懒惰的文档感到困惑,请search for doc issues with "lazy" in them 或"laziness",并将 cmets 添加到现有问题,或file a new doc issue(可能链接到此 SO 答案)。
如果您对提到懒惰的 Rakudo 错误消息感到困惑,请search for Rakudo issues with "lazy" in them, and tagged [LTA]
(which means "Less Than Awesome"),并添加 cmets 或 file a new Rakudo issue(带有 [LTA]
标签,也许还有指向此 SO 答案的链接)。
进一步讨论
文档...说“如果您想强制延迟评估,请使用
lazy
子例程或方法。绑定到标量或无符号容器也会导致惰性。”
是的。哎呀,这是正确的。
[which] 听起来像是暗示“
my $x := lazy gather ...
与my $x := gather ...
相同”。
没有。
显式的lazy
语句前缀或方法将强调添加到惰性,Raku 将其解释为意味着它应该阻止像.tail
这样的操作,以防它们挂起程序。
相比之下,绑定到变量既不会改变 emphasis 也不会de强调惰性,只会继续转发绑定的生产者数据类型/实例选择通过 @987654358 传达的任何内容@。
不仅与
gather
有关,在其他地方也是如此
是的。关于.is-lazy
的结果:
my $x = (1, .say; $_ + 1 ... 1000);
my $y = lazy (1, .say; $_ + 1 ... 1000);
两者都是懒惰的……但
$x.tail
是可能的,而$y.tail
是不可能的。
是的。
显式 lazy
语句前缀或方法强制.is-lazy
的答案为True
。这向关心懒惰危险的消费者发出信号,表明它应该变得谨慎(例如拒绝.tail
等)。
(相反,eager
语句前缀或方法可用于强制.is-lazy
的答案为False
,使胆小的消费者接受.tail
等调用。)
由此我认为,Raku 有两种懒惰,一种必须小心,看哪一种被用在了哪里。
我称之为消费引导的两种:
Don't-tail-me 如果一个对象从.is-lazy
调用返回True
,则它被视为可能是无限的。因此像.tail
这样的操作会被阻止。
You-can-tail-me如果对象从.is-lazy
调用返回False
,则接受.tail
之类的操作。
小心这两种类型中的哪一种在起作用并不需要太多,但如果想要调用像 tail
这样的操作,那么可能需要启用它插入eager
或删除lazy
,后果自负:
如果程序由于使用.tail
而挂起,那么,DIHWIDT。
如果你突然消耗了所有惰性序列并且没有缓存它,那么,也许你应该缓存它。
等等
我想说的是错误消息和/或文档可能需要改进。
【讨论】:
另外,我不知道这里的全部历史,作为 2015 年伟大列表重构的一部分,is-infinite
方法被替换为 is-lazy
(参见,例如,commit) .这为你所说的增加了证据^^^^:Raku并不真正知道列表是否是无限的,并且(现在)不会试图告诉你。它只会提供有关懒惰的信息,这更容易报告。
文档的问题是他们说“如果你想强制惰性求值,请使用惰性子例程或方法。绑定到标量或无符号容器将也强制惰性。” (强调我的)这对我来说仍然听起来像是“my $x := lazy gather ...
与 my $x := gather ...
相同”。
另外,在我看来,真正懒惰和有点懒惰的 Seq 之间的区别不仅可以在收集/获取方面找到,还可以在其他地方找到:my $x = (1, .say; $_ + 1 ... 1000)
和 my $y = lazy (1, .say; $_ + 1 ... 1000)
都在起作用懒惰(从某种意义上说,值是动态计算的),但$x.tail
是可能的,而$y.tail
不是。我由此得出,在 Raku 中存在两种懒惰,一种必须小心看哪一种被用在了哪里。【参考方案2】:
您要问的问题(“获取 [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
将返回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 角色和 PredictiveIterator 角色 - 而 PredictiveIterator
的主要观点是它确实知道它可以生成多少元素没有需要生产/迭代它们。但是PredictiveIterators
cannot be lazy。
第二个可能令人困惑的话题与第一个密切相关:虽然没有PredictiveIterator
可以是惰性的(也就是说,没有一个.is-lazy
方法会返回True
),但一些PredictiveIterator
s 有行为这与懒惰非常相似——事实上,甚至可以通俗地称为“懒惰”。
我无法很好地解释这种区别,因为老实说,我自己并不完全理解它。但我可以举个例子: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
s 填写一些细节,我将非常感激!
【讨论】:
我的问题可能表述错误。我实际上不在乎序列是否用尽。我想得到最后一个元素(然后完全忘记序列)。我可以使用for x in iterable: pass
之类的方法对 Python 可迭代对象执行类似的操作(在用完可迭代对象后,x
包含最后一个元素)。
用例是今天的 Advent of Code 作业(我正在使用今年的 AoC 来学习 Raku)。在我的解决方案中,我创建了一个gather
块,该块产生了所有获胜的宾果游戏板分数,我有兴趣即时生成第一个解决方案。这个想法是先做my $s = lazy gather ...
,然后是say $s[0]; say $s.tail
。无论如何,删除lazy 关键字是有效的(只要找到第一个中奖板,第一个值仍然会产生),虽然lazy gather
和(仍然有些懒惰)gather
之间的区别我还没有完全掌握.
@NikolaBenes 哦,如果你不需要/不想让列表变得懒惰,那么你可以让它eager
!所以,是的,放弃lazy
的工作,但如果你确实有一个惰性列表,你也可以使用$s.eager.tail
。或者,更短的方式让它变得渴望:$s[*].tail
。 (哦,如果您正在 Raku 中进行 Advent of Code,您可能有兴趣将您的解决方案添加到 community solution repo)
关于lines
的懒惰...曾经,在很久以前,有人向我解释过vi
扫描输入文件,记录每一行的文件偏移量,然后使用文件可见部分的偏移量,以将这些字节提取到 RAM 中(并且只有那些字节)。 (完全披露:我从来没有跟进看看是否有任何一个是真实的。它可能不是。它可能曾经是一次并且不再是真实的。YMMV。)所以vi
的.lines
等效内部扫描其隐式迭代以了解其长度并收集有关每个项目的一些元数据,但懒惰地评估每个项目。以上是关于在 Raku 中获取惰性 Seq 的最后一个元素的主要内容,如果未能解决你的问题,请参考以下文章