.net F#Seq的一个实现问题

.net F#Seq的一个实现问题,.net,haskell,f#,functional-programming,.net,Haskell,F#,Functional Programming,我最近在深入研究F#源代码 在Seq.fs中: // Binding. // // We use a type defintion to apply a local dynamic optimization. // We automatically right-associate binding, i.e. push the continuations to the right. // That is, bindG (bindG G1 cont1) cont2 --> bindG G1

我最近在深入研究F#源代码

在Seq.fs中:

// Binding. 
//
// We use a type defintion to apply a local dynamic optimization. 
// We automatically right-associate binding, i.e. push the continuations to the right.
// That is, bindG (bindG G1 cont1) cont2 --> bindG G1 (cont1 o cont2)
// This makes constructs such as the following linear rather than quadratic:
//
//  let rec rwalk n = { if n > 0 then 
//                         yield! rwalk (n-1)
//                         yield n }
看到上面的代码后,我测试了两个代码:

let rec rwalk n = seq { if n > 0 then 
                         yield n
                         yield! rwalk (n-1)
                      }

我发现第一个很快,而第二个很慢。如果n=10000,在我的机器上生成这个序列需要3秒,因此是二次时间

二次时间是合理的,例如:

seq{yield!{1;2;…;n-1};yield n}
转换为

Seq.append {1; 2; ...; n-1} {n}
我想这个追加操作应该需要线性时间。而在第一个代码中,追加操作是这样的:
seq{yield n;yield!{n-1;n-2;…;1}}
,这需要固定的时间

代码中的注释表示它是
线性的
(可能这个线性时间不是线性时间)。可能这
linear
与使用定制的序列实现而不是Moand/F#计算表达式有关(如F#规范中所述,但是规范没有提到这样做的原因…)

有人能澄清一下这里的模糊性吗?非常感谢


(另外,因为这是一个语言设计和优化问题,我还附加了Haskell标签,看看那里的人是否有见解。)

屈服时出现在非尾部呼叫位置,其基本含义与:

for v in <expr> do yield v
此外,每个递归调用都会创建一个新的对象实例(
IEnumerator
),因此也会有一些内存开销(尽管只是线性的)

尾部调用位置,F#编译器/librar进行优化。它用递归调用返回的元素“替换”当前的
IEnumerable
,因此它不需要对其进行迭代来生成所有元素,只需返回即可(这也消除了内存开销)


相关。同样的问题在C#lanaugage设计中已经讨论过,并且有一个(他们的
收益率!
的名字是
收益率foreach
)。

我不确定你在寻找什么样的答案。正如您所注意到的,注释与编译器的行为不匹配。我不能说这是一个注释与实现不同步的实例,还是它实际上是一个性能缺陷(例如,规范似乎没有提出任何特定的性能要求)

然而,从理论上讲,编译器的机器应该可以生成一个在线性时间内对示例进行操作的实现。事实上,甚至可以使用计算表达式在库中构建这样的实现。下面是一个粗略的例子,主要基于托马斯引用的论文:

opensystem.Collections
open System.Collections.Generic
类型“a nestedState=
///不让步
|完成
///在继续之前生成一个值
|瓦拉
///在继续之前,从嵌套迭代器中生成结果
|(单元->嵌套状态)的枚举
///仅从嵌套迭代器生成结果
|(单元->嵌套状态的尾部)
类型nestedSeq=
让堆栈=参考[ntor]
让curr=ref未选中。带
成员x.当前=!咖喱
接口System.IDisposable与
成员x.Dispose()=()
IEnumerator与的接口
成员x.MoveNext()=MoveNext()
成员x.当前=框!咖喱
成员x.Reset()=failwith“Reset not supported”}
成员x.NestedEmerator=ntor
接口IEnumerable=函数
| :? ('a nestedSeq)作为n->n.nestedEmerator
|s->
设e=s.GetEnumerator()
乐趣()->
如果e.MoveNext()那么
当前值
其他的
多恩
让状态(arr:Lazy)=
让状态=ref-1
nestedSeq(fun()->incr state;arr.Value.[!state]):>seq
键入SeqBuilder()
成员s.收益率(x)=
状态(惰性[| Val x;Done |])
成员s.Combine(x:'a seq,y:'a seq)=
状态(惰性[|枚举(GetNestedEmerator x);尾部(GetNestedEmerator y)|])
成员s.Zero()=
状态(惰性[|完成|])
成员s.Delay(f)=
状态(lazy[| Tail(f()|>getnestedumerator)|])
成员s.YieldFrom(x)=x
成员s.Bind(x:'a seq,f)=
设e=x.GetEnumerator()
nestedSeq(乐趣()->
如果e.MoveNext()那么
枚举(如当前|>getNestedEnumerator)
其他的
完成):>如下
设seq=SeqBuilder()
设rec walkr n=seq{
如果n>0,则
返回!walkr(n-1)
返回n
}
让rec walkl n=seq{
如果n>0,则
返回n
返回!walkl(n-1)
}
让时间=
让watch=System.Diagnostics.Stopwatch.StartNew()
walkr 10000 |>序列iter忽略
看,停
看,已经过去了
请注意,my
SeqBuilder
不是健壮的;它缺少几个工作流成员,并且不做任何关于对象处理或错误处理的事情。然而,它确实证明了
SequenceBuilder
s不需要在像您这样的示例上显示二次运行时间


还要注意,这里有一个时空折衷-walkr n的嵌套迭代器将在O(n)时间内迭代序列,但这需要O(n)空间。

+1用于挖掘源代码感谢本文!F#seq有3种状态:屈服、停止和转到。而报纸上有4个。额外的一个已经完成了。最后一种状态允许深度搜索/回溯式迭代递归屈服结构,它实际上是一棵树。对于任何好奇的人来说,
yield foreach
是C#语言未来的一种潜在扩展,在VS11中没有实现。
for v in <expr> do yield v
x
x x 
x x x
x x x x