看了这两个帖子后:Does F# have an equivalent to Haskell's take?,Take N elements from sequence with N different indexes in F# ,我一直想知道在列表上使用序列运算符的最佳方法,甚至是使用它们。
我现在是F#的新手,我正在编写一个程序,它必须处理从HtmlAgilityPack获得的大量序列。 Seq模块中有一些有趣的运算符,但正如那些线程所述,它可能与性能有关,如果我们不得不在seq - >之间不断转换。列出它也会使代码混乱,而不是解决问题...这就是我开始学习F#的原因。
一个简单的例子是当我需要取一个列表的'N'个元素时:
listOfRows
|> Seq.take 2
// Now I don't have a list anymore, it returns a sequence
|> List.ofSeq
那么,是否有人能够了解处理这些情景的最佳方法?我可以使用Seq.take和Seq.skip来处理解决方案,但这已知效率非常低。另一方面,将功能内置到标准库中并且必须重新实现它以在不同集合上使用相同的概念,或者通过显式转换使代码更脏,这是一种耻辱。
我怎样才能看到'list - >之间每次转换对性能的影响? seq'和'seq - >列表'有?
非常感谢。
答案 0 :(得分:6)
这些实施起来相当简单。
module List =
let take n items =
let rec take' acc = function
| 0, _ -> List.rev acc
| _, [] -> invalidOp "count exceeds number of elements"
| n, h::t -> take' (h::acc) (n-1, t)
take' [] (n, items)
let rec skip n items =
match n, items with
| 0, _ -> items
| _, [] -> invalidOp "count exceeds number of elements"
| n, _::t -> skip (n-1) t
以下是他们与Seq
同行的表现。
let n = 10000000
let l = List.init n id
let test f = f (n-1) l
test List.take //Real: 00:00:03.724, CPU: 00:00:03.822, GC gen0: 57, gen1: 34, gen2: 1
test Seq.take |> Seq.toList //Real: 00:00:04.953, CPU: 00:00:04.898, GC gen0: 57, gen1: 33, gen2: 0
test List.skip //Real: 00:00:00.044, CPU: 00:00:00.046, GC gen0: 0, gen1: 0, gen2: 0
test Seq.skip |> Seq.toList //Real: 00:00:01.147, CPU: 00:00:01.154, GC gen0: 0, gen1: 0, gen2: 0
如果您的应用程序计算毫秒数,那么创建“缺失”List
函数可能是值得的。否则,我会说使用Seq
版本完全没问题。
答案 1 :(得分:3)
其中一些可能取决于您想要如何使用所有这些端到端。
在许多情况下,可以预先转换为列表,然后只使用List运算符来映射/遍历/ etc。可能没有List.take
,但这是因为对于列表,如果您知道将至少有两个元素并且您想要抓住这两个元素,则可以使用模式匹配来完成,例如
let (item1::item2::rest) = someList
所以我怀疑这可能是你想要在这种情况下做的事情(我希望通过HTML解析,你可能会有一些你正在寻找的元素的预期粗略模式等。)
(如果懒惰/流媒体是必不可少的,那么Seq会变得更有用。)
简而言之,最常见的运算符(如map
)位于所有集合类型(Seq
,List
,Array
,...)上,而不常见的运算符(像take
)只能在Seq上使用,通常是因为当你有一个具体的类型时有更好的方法来做事情(例如列表模式匹配以获取第一项)。
答案 2 :(得分:2)
添加评论
在纯粹的功能意义上take
无法在列表中运行 - 请考虑
a::b::c::d::[]
如果我们只想要前两个元素,我们至少需要修改b
以便我们得到
a::b::[]
自b
修改后,您还需要修改a
,以便它指向新修改的b
。因此,无法在列表上实现这一点,这解释了为什么List
模块中缺少它。
如果您真的担心性能,请首先进行配置,然后考虑切换到不同的数据类型。你可能最好使用.Net System.Collections.Generic.List<_>
,它有许多与List
和Array
- http://research.microsoft.com/en-us/um/cambridge/projects/fsharp/manual/fsharp.powerpack/microsoft.fsharp.collections.resizearray.html
答案 3 :(得分:2)
您可以完全理解转化效果的性能影响Seq - &gt;列表和列表 - &gt; Seq通过检查相应的转换实现:
// List.ofSeq
let ofSeq source = Seq.toList source
// List.toSeq
let toSeq list = Seq.ofList list
// Seq.ofList
let ofList (source : 'T list) =
(source :> seq<'T>)
// Seq.toList
let toList (source : seq<'T>) =
checkNonNull "source" source
match source with
| :? ('T list) as res -> res
| :? ('T[]) as res -> List.ofArray res
| _ ->
use e = source.GetEnumerator()
let mutable res = []
while e.MoveNext() do
res <- e.Current :: res
List.rev res
与集合上的实际操作相比,转换本身对性能的影响相对较小。运行以下代码片段,将100万成员的List转换为seq,然后返回到我的旧Core 2 Duo 2.4Ghz笔记本上的另一个List
open System.Diagnostics
let tls = Stopwatch()
let l = [1..1000000]
tls.Start()
let s = List.toSeq l
//Seq.length s |> ignore
//Seq.length s |> ignore
tls.Stop()
printfn "List<int> of 1000000 -> Seq: %d ticks" tls.ElapsedTicks
let tsl = Stopwatch()
tsl.Start()
let l' = Seq.toList s
//l'.Length |> ignore
//l'.Length |> ignore
tsl.Stop()
printfn "Seq<int> of 1000000 -> List: %d ticks" tsl.ElapsedTicks
相应地显示爆破42和8蜱。如果我们使用长度计数器取消注释第一个相应的行,则执行将需要18695和12952个滴答。在取消注释具有长度计数器的第二个相应行后,执行持续时间显示38377和25404个滴答,这表示懒惰与观察到的散乱无关。
与集合操作执行本身相比,Seq和List类型之间的转换开销似乎可以忽略不计。
答案 4 :(得分:1)
List to Seq只是在List上创建一个迭代器(在.net世界中是一个Enumerable),所以基本上它不是一个会导致很多性能问题的操作(它只是创建一个保持状态的状态机)关于哪个是列表中当前要素'yield'的元素,以及当请求更多元素时它的增量。另一方面,将Seq(它将具有一些其产生值的基础集合)转换为List在概念上与迭代列表并从中创建新列表相同,因此它可能是一个耗费时间和内存的过程,以防万一列表足够长。
就这些运算符的使用而言,一种可能的方法是将所有序列运算符组合在一起(与linq查询相同,其中您创建一个管道,通过该管道逐个处理集合元素)然后在如果需要,可以从结果Seq创建列表,因为列表是在seq上的所有过滤,映射,处理等工作结束时创建的,当最终数据准备就绪时,您将其转换为List。创建中间列表不会很好,会导致性能问题。