【问题标题】:How to write efficient list/seq functions in F#? (mapFoldWhile)如何在 F# 中编写高效的列表/序列函数? (mapFoldWhile)
【发布时间】:2016-06-28 03:57:40
【问题描述】:

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

我不想使用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# 的不可变列表实际上是可变的,因为性能?我有点担心这一点,因为我认为这是 F# 中的“必经之路”,因此使用了 prepend-then-reverse list 方法。

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

我经常发现自己需要“提前退出”,因为某个收藏项目“无效”,而我知道我不必查看其他内容。在某些情况下我使用List.pickSeq.takeWhile,但在其他情况下我需要做更多的事情(mapFold)。

对于此类问题是否有有效的解决方案(特别是mapFoldWhile,通常是“提前退出”)与函数式编程概念,还是我必须切换到命令式解决方案/使用Collections.Generics.List

【问题讨论】:

  • 如果您不需要使用List&lt;T&gt;,您可以在mapFoldWhile 内部使用ResizeArray&lt;T&gt; 并返回T[]。因此,您正在使用可变性构建结果,但函数的 API 是不可变的(这是 F# 在内部所做的)。或者您使用高效的流媒体库,例如 Nessos Streams
  • 定义高效。如果您的文件夹功能足够昂贵以保证提前退出,那么您可能没有理由关心List.rev。你量过吗?
  • @scrwtp 是的,这似乎是一个比昨天更大的问题(一个漫长的周末之后的一个漫长的星期一......)。当我意识到 List.rev 没有被其他类似 map 的函数使用时,我感到措手不及,担心它可能会很慢,然后找不到替代方法。我确实相信提前退出应该很容易实现,为此 Tomas 指出了一个很好的解决方案。

标签: f#


【解决方案1】:

在大多数情况下,使用List.rev 是一个完全足够的解决方案。

F# 核心库使用突变和其他肮脏的技巧来从 F# 列表操作中挤出更多性能是对的,但我认为在那里进行的微优化并不是特别好的例子。 F# 列表函数几乎无处不在,因此它可能是一个很好的权衡,但在大多数情况下我不会遵循它。

使用以下命令运行您的函数:

let l = [ 1 .. 1000000 ]

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

当我在没有更改的情况下运行该函数时,我在第二行得到了 ~240 毫秒。当我删除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 会在退出条件发生时立即停止计算。

【讨论】:

  • 我完全忘记了Seq函数的懒惰,谢谢提醒!如果我可以问一个快速的后续问题:在这种情况下,一般来说什么更合适——编写map 函数的自定义版本或使用Seq 函数(从函数式编程的角度来看)?我主要需要提前退出,因为我不希望在达到无效状态后再次调用我的状态函数。 (并且担心mapFoldWhile 的性能,因为我希望它与map 一样广泛适用,因此它不应该变得更糟——但反应过度了一点)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-02-15
  • 1970-01-01
  • 2019-02-12
  • 1970-01-01
  • 2014-05-10
相关资源
最近更新 更多