如何在F#中编写高效的list / seq函数? (mapFoldWhile)

时间:2016-06-27 16:57:18

标签: f#

我试图编写一个通用的mapFoldWhile函数,它只是mapFold但要求stateoption并在遇到{None后立即停止{1}}州。

我不想使用mapFold,因为它会转换整个列表,但我希望它一旦找到无效状态(即None)就停止。

这是我的第一次尝试:

let mapFoldWhile (f : 'State option -> 'T -> 'Result * 'State option) (state : 'State option) (list : 'T list) =
  let rec mapRec f state list results =
    match list with 
    | [] -> (List.rev results, state)
    | item :: tail -> 
      let (result, newState) = f state item
      match newState with 
      | Some x -> mapRec f newState tail (result :: results)
      | None -> ([], None)
  mapRec f state list []

List.rev使我烦恼,因为练习的目的是提前退出并构建新的列表应该更慢。

所以我查看了F#自己的map做了什么,这是:

let map f list = Microsoft.FSharp.Primitives.Basics.List.map f list

可以找到不祥的Microsoft.FSharp.Primitives.Basics.List.map here,如下所示:

let map f x = 
    match x with
    | [] -> []
    | [h] -> [f h]
    | (h::t) -> 
        let cons = freshConsNoTail (f h)
        mapToFreshConsTail cons f t
        cons

consNoTail内容也在此文件中:

// optimized mutation-based implementation. This code is only valid in fslib, where mutation of private
// tail cons cells is permitted in carefully written library code.
let inline setFreshConsTail cons t = cons.(::).1 <- t
let inline freshConsNoTail h = h :: (# "ldnull" : 'T list #)

所以我猜测F#的不可变列表实际上是可变的,因为性能?我有点担心这一点,使用了prepend-then-reverse list方法,因为我认为这是&#34;方式去&#34;在F#。

我对F#或一般的函数式编程不是很有经验,所以也许(可能)创建一个新的mapFoldWhile函数的整个想法是错误的,但那么我是什么意思反而呢?

我经常发现自己处于需要及早退出的状态。因为收集项目是&#34;无效&#34;我知道我不必看其余的。我在某些情况下使用List.pickSeq.takeWhile,但在其他情况下,我需要做更多(mapFold)。

是否有一个有效的解决方案来解决这类问题(特别是mapFoldWhile和#34;早期退出&#34;一般情况下)使用函数式编程概念,或者我是否必须切换到命令式解决方案/使用Collections.Generics.List

1 个答案:

答案 0 :(得分:8)

在大多数情况下,使用List.rev是一个非常充分的解决方案。

你是对的,F#核心库使用变异和其他脏黑客来从F#list操作中挤出更多性能,但我认为那里进行的微优化不是特别好的例子。 F#list函数几乎无处不在使用,所以它可能是一个很好的权衡,但在大多数情况下我都不会遵循它。

使用以下命令运行您的功能:

let l = [ 1 .. 1000000 ]

#time 
mapFoldWhile (fun s v -> 0, s) (Some 1) l

当我在没有更改的情况下运行该函数时,我在第二行得到~240ms。当我只是删除List.rev(以便它以其他顺序返回数据)时,我大约需要190毫秒。如果你真的经常调用这个函数,这很重要,那么你必须使用变异(实际上,你自己的可变列表类型),但我认为这很少值得。

对于一般的“退出早期”问题,您通常可以将代码编写为Seq.scanSeq.takeWhile的组合。例如,假设您要对序列中的数字求和,直到达到1000.您可以写:

input
|> Seq.scan (fun sum v -> v + sum) 0
|> Seq.takeWhile (fun sum -> sum < 1000)

使用Seq.scan生成一系列总和超过整个输入,但由于这是生成延迟,因此使用Seq.takeWhile会在退出条件发生后立即停止计算。