F#中的Seq.zip可能需要看似额外的序列元素才能完成

时间:2011-08-29 04:15:45

标签: haskell f#

让Seq.zip两个F#序列,一个由列表表示,另一个 - 由Seq.filter应用于无限序列:

Seq.initInfinite (fun i -> i)
|> Seq.filter ((>) 3)
|> Seq.zip ["A";"B"]

按预期返回

val it : seq<string * int> = seq [("A", 0); ("B", 1)]

然而,

Seq.initInfinite (fun i -> i)
|> Seq.filter ((>) 2)
|> Seq.zip ["A";"B"]

试图获得可以通过Seq.filter并最终炸毁fsi的不存在的第3个成员:

 Error: Enumeration based on System.Int32 exceeded System.Int32.MaxValue.

虽然由字母列表表示的另一个参数暗示只有两个过滤后的元素足以对函数规范执行zip。

如果我们转向Haskell进行实现比较等效

 zip ["A","B"] (filter (<2) [0..])

完成没有任何问题产生

[("A",0),("B",1)]

由于Haskell实现行为看起来直观正确F#Seq.zip实现的观察行为的理由是什么?

更新:

我没注意到Haskell

zip (filter (<2) [0..]) ["A","B"]

与F#不完全相同。

底线:无法实现能够以参数顺序无关的方式压缩定义和未定义长度序列的Zip函数。 F#Zip实现只是优先于Haskell参数顺序依赖的参数顺序行为的不变量。

4 个答案:

答案 0 :(得分:6)

我不知道Haskell是如何做到的,我同意它看起来直观正确(除了我想知道如果你切换固定长度列表和不确定长度列表会在Haskell中发生什么),但我可以告诉你为什么它在F#中以这种方式工作。您可以在F#源代码文件seq.fs中看到重要的实施细节位于IEnumerable.map2中:

  let map2 f (e1 : IEnumerator<_>) (e2 : IEnumerator<_>) : IEnumerator<_>=
      upcast 
          {  new MapEnumerator<_>() with
                 member this.DoMoveNext curr = 
                    let n1 = e1.MoveNext()
                    let n2 = e2.MoveNext()
                    if n1 && n2 then
                       curr <- f e1.Current e2.Current
                       true
                    else 
                       false
                 member this.Dispose() = e1.Dispose(); e2.Dispose()
          }

所以Seq.zip会在决定拉链是否完整之前尝试将两个序列移动到第三个元素,因此Seq.initInfinite (fun i -> i) |> Seq.filter ((>) 2)卡住试图找到第三个元素“永远”(直到{{1} })。

答案 1 :(得分:6)

它不会在Haskell中挂起的原因是因为zip的实现恰好在它的第一个参数中比在第二个参数中更严格。

zip :: [a] -> [b] -> [(a,b)]
zip (a:as) (b:bs) = (a,b) : zip as bs
zip _      _      = []

由于从左到右检查模式,因此会产生以下行为。

*Main> zip [] undefined
[]
*Main> zip undefined []
*** Exception: Prelude.undefined

由于filter (<2) [0..]在语义上等同于0 : 1 : ⊥,因此您的示例是在两次迭代之后

("A", 0) : ("B", 1) : zip [] undefined =
("A", 0) : ("B", 1) : []

如果我们将参数的顺序更改为zip (filter (<2) [0..]) ["A", "B"],我们就会得到

(0, "A") : (1, "B") : zip undefined [] =
(0, "A") : (1, "B") : undefined

我对F#了解不多,但我怀疑那里发生了类似的情况。

请注意,无法定义zipzip [] undefinedzip undefined []都返回[],因为您必须先检查其中一个参数,然后才能因monotonicity而无法检查⊥。

答案 2 :(得分:0)

斯蒂芬斯文森已经回答了这个问题。

现在的解决方案似乎使用了Seq.take,因为你知道其中一个序列的长度。

Seq.initInfinite (fun i -> i)
|> Seq.filter ((>) 2)
|> Seq.zip ["A";"B"]
|> Seq.take 2

答案 3 :(得分:0)

基于我对源代码的解读(https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSharp.Core/seq.fs,大约在900行),这就是:

相关函数在Seq.fs中并被称为revamp2(这是Seq.zip调用的)

let revamp2 f (ie1 : seq<_>) (source2 : seq<_>) =
        mkSeq (fun () -> f (ie1.GetEnumerator()) (source2.GetEnumerator()))

现在,当我们对此返回的序列调用.MoveNext()时,它会在输入序列的两个上调用MoveNext()。

以这种方式执行此操作已使其他许多代码变得更简单但导致了您的问题 - .MoveNext()将不会返回已过滤的无限序列,但会返回有限序列,从而导致无限循环。