F#折叠三元组中的序列

时间:2011-04-15 15:32:15

标签: f# fold

我用Google搜索并阅读,并且我正在尝试找到一种“正确”的方法,但我在SO上阅读的每个问题似乎都有完全不同的答案。

这是我的问题的要点。 files具有三元组seq的类型签名(a:string,b:string,c:Int64)。作为f#的新手,我仍然不能流利地表达类型签名(或者理解它们)。 a是文件名,b是内部标识符,c是表示文件长度(大小)的值。 baseconfig是代码中较早的字符串。

ignore(files 
    |> Seq.filter( fun(x,y,z) ->  y = baseconfig)  // used to filter only files we want
    |> Seq.fold( fun f n   -> 
        if( (fun (_,_,z) -> z) n > 50L*1024L*1024L) then
            zipfilex.Add((fun (z:string, _, _) -> z) n)
            printfn("Adding 50mb to zip")
            zipfilex.CommitUpdate()
            zipfilex.BeginUpdate()
            ("","",0L)
        else
            zipfilex.Add((fun (z, _, _) -> z) n)
            ("", "", (fun (_, _, z:Int64) -> z) n + (fun (_, _, z:Int64) -> z) f)
    ) ("","",0L)
    )

这段代码应该做的是,遍历files中的每个文件,将其添加到zip存档(但不是真的,它只是列在稍后提交的列表中),以及何时文件超过50MB,将当前挂起的文件提交到zip存档。添加文件很便宜,提交成本很高,所以我尝试通过批处理来降低成本。

到目前为止代码有点工作......除了我接近150MB提交文件时得到的ObjectDisposedException。但我不确定这是做这种手术的正确方法。感觉就像我以非常规的方式使用Seq.fold,但是,我不知道有更好的方法来做到这一点。

奖金问题:有没有更好的方法来剔除元组中的值? fst和snd只适用于2个有价值的元组,我意识到你可以定义自己的函数而不是像我一样内联它们,但似乎应该有更好的方法。

更新:我之前尝试折叠,我无法理解为什么我不能只使用Int64作为累加器。事实证明我错过了一些关键的括号。下面的简单版本更简单。还消除了所有疯狂的元组提取。

ignore(foundoldfiles 
    |> Seq.filter( fun (x,y,z) ->  y = baseconfig) 
    |> Seq.fold( fun (a) (f,g,j)   -> 
        zipfilex.Add( f)
        if( a > 50L*1024L*1024L) then
            printfn("Adding 50mb to zip")
            zipfilex.CommitUpdate()
            zipfilex.BeginUpdate()
            0L
        else
             a + j
    ) 0L
    )

更新2 :我将不得不采用命令式解决方案,在声明中关闭zip文件后,F#以某种方式重新输入此代码块跟着它。这解释了ObjectDisposedException。不知道它是如何工作的或原因。

4 个答案:

答案 0 :(得分:4)

作为“脏”命令式样式的替代方法,您可以使用常规和可重用的函数扩展Seq模块以进行分块。该函数有点像fold,但它需要一个返回option<'State>的lambda。如果它返回None,则启动新块,否则将元素添加到前一个块。然后你可以写一个优雅的解决方案:

files
|> Seq.filter(fun (x, y, z) ->  y = baseconfig) 
|> Seq.chunkBy(fun (x, y, z) sum -> 
     if sum + z > 50L*1024L*1024L then None
     else Some(sum + z)) 0L
|> Seq.iter(fun files ->
    zipfilex.BeginUpdate()
    for f, _, _ in files do zipfilex.Add(f)
    zipfilex.CommitUpdate())

chunkBy函数的实现时间稍长 - 需要直接使用IEnumerator&amp;它可以用递归表示:

module Seq = 
  let chunkBy f initst (files:seq<_>) = 
    let en = files.GetEnumerator()
    let rec loop chunk st = seq {
      if not (en.MoveNext()) then
        if chunk <> [] then yield chunk
      else
        match f en.Current st with
        | Some(nst) -> yield! loop (en.Current::chunk) nst
        | None -> 
            yield chunk 
            yield! loop [en.Current] initst }
    loop [] initst

答案 1 :(得分:2)

我认为您的问题不会因使用fold而受益。它在构建不可变结构时最有用。在这种情况下,我的观点是,它使你想要做的事情变得不那么明确。必要的解决方案非常有效:

let mutable a = 0L
for (f, g, j) in foundoldfiles do
    if g = baseconfig then
        zipfilex.Add(f)
        if a > 50L * 1024L * 1024L then
            printfn "Adding 50mb to zip"
            zipfilex.CommitUpdate()
            zipfilex.BeginUpdate()
            a <- 0L
        else
            a <- a + j

答案 2 :(得分:1)

这是我的看法:

let inline zip a b = a, b

foundoldfiles 
|> Seq.filter (fun (_, internalid, _) -> internalid = baseconfig)
|> zip 0L
||> Seq.fold (fun acc (filename, _, filesize) -> 
    zipfilex.Add filename
    let acc = acc + filesize
    if acc > 50L*1024L*1024L then
        printfn "Adding 50mb to zip"
        zipfilex.CommitUpdate ()
        zipfilex.BeginUpdate ()
        0L
    else acc)
|> ignore

一些注意事项:

  • zip辅助函数通过整个函数清理管道而没有任何开销,并且在更复杂的情况下有助于类型推理,因为状态从{{的右侧移动到左侧1}}仿函数(虽然在这种特殊情况下无关紧要或帮助)
  • 使用fold本地丢弃您不需要的元组元素,使代码更易于阅读
  • 流水线化为_而不是用额外的括号包装整个表达式的方法使代码更容易阅读
  • 在括号中包含一元函数的参数看起来很奇怪;你不能对非一元curried函数使用括号,因此将它们用于一元函数是不一致的。我的策略是为构造函数调用和tupled函数调用保留括号

编辑:P.S。 ignore是不正确的逻辑 - if( a > 50L*1024L*1024L) then需要考虑累加器加上当前的文件大小。例如,如果第一个文件是> = 50MB,则if不会触发。

答案 3 :(得分:1)

如果你不喜欢可变变量和命令式循环,你总是可以使用 GOTO 重写这个函数循环:

let rec loop acc = function
    | (file, id, size) :: files ->
        if id = baseconfig then
            zipfilex.Add file
            if acc > 50L*1024L*1024L then
                printfn "Adding 50mb to zip"
                zipfilex.CommitUpdate()
                zipfilex.BeginUpdate()
                loop 0L files
            else
                loop (acc + size) files
        else
            loop acc files
    | [] -> ()

loop 0L foundoldfiles

这样做的好处是它明确地说明了归纳案例可以进行的三种不同方式以及在每种情况下如何转换累加器(因此你不太可能弄错了 - 见证Daniel's for loop version中的错误)。

您甚至可以将baseconfig检查移动到when子句:

let rec loop acc = function
    | (file, id, size) :: files when id = baseconfig ->
        zipfilex.Add file
        if acc > 50L*1024L*1024L then
            printfn "Adding 50mb to zip"
            zipfilex.CommitUpdate()
            zipfilex.BeginUpdate()
            loop 0L files
        else
            loop (acc + size) files
    | _ :: files -> loop acc files
    | [] -> ()

loop 0L foundoldfiles