【问题标题】:Ambiguous use of 'lazy'模棱两可地使用“懒惰”
【发布时间】:2017-09-16 18:39:34
【问题描述】:

我不知道为什么this example 是模棱两可的。 (我很抱歉没有在这里添加代码,它太长了。)

我已将prefix (_ maxLength) 作为重载添加到LazyDropWhileBidirectionalCollectionsubscript(position)LazyPrefixCollection 上定义。然而,上面示例中的以下代码不应该是模棱两可的,但它是:

print([0, 1, 2].lazy.drop(while: {_ in false}).prefix(2)[0]) // Ambiguous use of 'lazy'

据我了解,协议层次结构中较高的重载将被使用。

根据编译器无法在两种类型之间进行选择;即LazyRandomAccessCollectionLazySequence。 (这没有意义,因为subscript(position) 不是LazySequence 的方法。)LazyRandomAccessCollection 将是这里的合乎逻辑的选择。

如果我删除下标,它会起作用:

print(Array([0, 1, 2].lazy.drop(while: {_ in false}).prefix(2))) // [0, 1]

可能是什么问题?

【问题讨论】:

  • 编译器输出显示了lazy的两个可能候选对象
  • 能否请您发布相关的“问题代码”?
  • @shallowThought 相关问题代码是第一句链接的最后一行。我已将其添加到上面的帖子中。

标签: swift lazy-evaluation overloading swift4


【解决方案1】:

这里的线索太复杂和模棱两可了。您可以通过删除元素来看到这一点。特别是,删除最后一个下标:

let z = [0, 1, 2].lazy.drop(while: {_ in false}).prefix(2)

在此配置中,编译器希望将z 键入为LazyPrefixCollection<LazyDropWhileBidirectionalCollection<[Int]>>。但这不能被整数索引。 我知道感觉应该是这样,但当前编译器无法证明这一点。(见下文)所以你的[0] 失败了。而且回溯还不足以摆脱这个疯狂的迷宫。具有不同返回类型的重载太多了,编译器不知道你想要哪一个。

但是这个特殊情况很容易解决:

print([0, 1, 2].lazy.drop(while: {_ in false}).prefix(2).first!)

也就是说,我绝对会避免如此努力地推动编译器。这对今天的 Swift 来说太聪明了。特别是返回不同类型的重载在 Swift 中通常是一个坏主意。当它们很简单时,是的,你可以摆脱它。但是当您开始将它们分层时,编译器没有足够强大的证明引擎来解决它。 (也就是说,如果我们研究的时间足够长,我敢打赌它实际上是模棱两可的,但诊断具有误导性。当您进入过于聪明的 Swift 时,这是一种非常常见的情况。)


既然您描述了它(在 cmets 中),推理就很简单了。

LazyDropWhileCollection 不能有整数索引。索引下标要求为 O(1)。这就是Index 下标相对于其他下标的含义。 (Index 下标还必须返回 Element 类型或崩溃;它不能返回 Element?。这样就有一个与 Key 分开的 DictionaryIndex。)

由于集合是惰性的并且有任意数量的缺失元素,查找任何特定的整数“计数”(第一个、第二个等)都是 O(n)。如果不遍历至少 100 个元素,就不可能知道第 100 个元素是什么。要成为一个集合,它的 O(1) 索引必须采用只能通过先前遍历序列来创建的形式。不能是Int

这很重要,因为当您编写如下代码时:

for i in 1...1000 { print(xs[i]) }

您希望它大约为 1000 个“步”,但如果此集合有一个整数索引,那么它将大约为 100 万步。通过包装索引,它们会阻止您首先编写该代码。

这在 Swift 等高度通用的语言中尤其重要,因为通用算法层很容易将意外的 O(n) 操作级联成完全不可行的性能(我所说的“不可行”是指您预计需要几分钟才能完成的事情)或更多)。

【讨论】:

  • 感谢您的回答。经过一番挖掘,[0] 失败的原因可能与drop(while:)(和prefix(while:))将底层索引包装在LazyDropWhileIndex(和LazyPrefixWhileIndex)中有关。这是一个struct,它有一个隐含的init(因此不能在标准库之外初始化)。符合LazyCollectionProtocol 的所有其他类型不包装它们的索引,而是“转发”基础基础集合的索引。 (包装可能有充分的理由,我还没有完全理解。)
  • 感谢您的编辑。还有一些想法。 LazyPrefixWhileIndex 做了一个很酷的把戏,endIndex 忽略基本集合的索引,只返回一个 enum 值,表示 O(1) 中的结束索引。为涉及LazyPrefixWhileIndex 的任何内容编写单元测试是不可能的,因为它的init 是隐式的,并且保存索引表示的底层值是internal。使用LazyDropWhileIndex,我们至少可以访问base 索引,因此我们可以针对基本集合的Index 类型的实例对其进行测试。
  • 总之,我可以理解LazyPrefixWhileIndex 背后的原因,但是对于LazyDropWhileIndex,他们可以只使用底层的Index 类型。
  • 你将如何在 O(1) 中实现它?在找到第一个未删除的元素之前,您可能必须删除整个集合。除非有缓存,否则查找xs[1] 可能必须再次执行所有操作(但无论如何仍将是 O(n),因为它可能必须扫描集合的其余部分)。
  • 最后两个 cmets 主要是对 LazyDropWhileIndexLazyPrefixWhileIndex 的观察。在第一种情况下,Index 类型无关紧要,因为它不会做任何其他Index 类型可以做的“额外”事情。不需要包装器; LazyDropWhileIndex(1)1 完全相同,除非我遗漏了什么。
【解决方案2】:

把最后一行改成这样:

let x = [0, 1, 2]

let lazyX:  LazySequence                = x.lazy
let lazyX2: LazyRandomAccessCollection  = x.lazy
let lazyX3: LazyBidirectionalCollection = x.lazy
let lazyX4: LazyCollection              = x.lazy

print(lazyX.drop(while: {_ in false}).prefix(2)[0])

您会注意到该数组有 4 种不同的惰性构造 - 您必须是明确的。

【讨论】:

  • 据我了解,协议链中较高的重载将被使用。根据编译器,它不能在两种类型之间进行选择;即LazyRandomAccessCollectionSequence。 (这没有意义,因为 subscript(position) 不是 Sequence 的方法。)在这种情况下,LazyRandomAccessCollection 将是合乎逻辑的选择。
猜你喜欢
  • 1970-01-01
  • 2011-12-31
  • 1970-01-01
  • 2019-08-02
  • 1970-01-01
  • 2010-11-30
  • 1970-01-01
  • 1970-01-01
  • 2010-09-26
相关资源
最近更新 更多