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

Haskell F#中的Seq.zip可能需要额外的序列元素才能完成,haskell,f#,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.filter并最终炸毁fsi: Err

让我们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.filter并最终炸毁fsi:

 Error: Enumeration based on System.Int32 exceeded System.Int32.MaxValue.
尽管由字母列表表示的另一个参数暗示,仅两个过滤元素就足以执行函数规范的压缩

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

 zip ["A","B"] (filter (<2) [0..])
由于Haskell实现行为似乎直观地正确,那么F#Seq.zip实现的观察行为的理由是什么

更新:

我没有注意到哈斯克尔

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

zip(filter(我不知道Haskell是怎么做的,我也同意它看起来直观上是正确的(除非我想知道如果你切换固定长度列表和不确定长度列表,Haskell会发生什么),但我可以向您展示为什么它在F#中以这种方式工作。您可以在F#源代码文件中看到,重要的实现细节位于
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()
          }
让map2f(e1:IEnumerator)(e2:IEnumerator):IEnumerator=
上抛
{使用
成员this.DoMoveNext curr=
设n1=e1.MoveNext()
设n2=e2.MoveNext()
如果n1和n2,则

curr i)|>Seq.filter((>)2)
在尝试查找第三个元素“forever”(直到
错误:基于System.Int32的枚举超过了System.Int32.MaxValue
)。

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

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

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

根据我对来源的阅读(https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSharp.Core/seq.fs 大约在第900行)发生的情况如下:

相关的函数在Seq.fs中,称为reform2(这就是Seq.zip所称的函数)

2f(ie1:seq)(来源2:seq)=
mkSeq(fun()->f(ie1.GetEnumerator())(source2.GetEnumerator())
现在,当我们对由此返回的序列调用.MoveNext()时,它会对两个输入序列调用MoveNext()

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

这样做会使其他代码更简单,但也会导致您的问题-.MoveNext()不会为过滤后的无限序列返回,但会为有限序列返回,从而导致无限循环。

它不挂起在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(似乎与我使用的源代码版本不同,但有趣的是,如果将其更改为
if e1.MoveNext()&&e2.MoveNext
,它将工作一半的时间(有限序列为e1)@jpalmer-是的,我注意到最近有很多人链接到github源代码,但这是针对F#的单端口,因为我在Windows中工作,我查看了原始的codeplex源代码。@jpalmer-是的,这会起作用。但我倾向于相信F#设计者故意选择了当前的实现,因为这会有点出乎意料为
Seq.zip
的参数顺序歌唱implementation detail以改变其行为。@斯蒂芬·斯文森:也许原始问题中的“justification”一词不是最好用的,但我确实从阅读源代码中已经知道了F#实现的细节。我的首字母(错)假设Haskell实现对参数顺序是不变的:(关于您的最后一段,可能会有兴趣。@kvb:Good find!注意
unab
必须“欺骗”使用
unsafePerformIO
来实现这一点。@hammar:糟糕的我错过了检查参数的其他顺序。总结:只要我们不偏离顺序执行范例,就不可能实现能够以参数顺序无关的方式压缩已定义和未定义长度的序列的Zip函数。F#Zip impl校正只是喜欢一致的参数顺序不变行为,而不是Haskell参数顺序相关行为。
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
("A", 0) : ("B", 1) : zip [] undefined =
("A", 0) : ("B", 1) : []
(0, "A") : (1, "B") : zip undefined [] =
(0, "A") : (1, "B") : undefined