繁体   English   中英

F#将一个序列映射到另一个较短长度的序列

[英]F# map a seq to another seq of shorter length

我有这样的字符串序列(文件中的行)

[20150101] error a
details 1
details 2
[20150101] error b
details
[20150101] error c

我正在尝试将其映射到这样的字符串序列(日志条目)

[20150101] error a details 1 details 2
[20150101] error b details
[20150101] error c

我可以通过命令式方式(通过翻译我将用C#编写的代码)来做到这一点-可以工作,但它的读取方式类似于伪代码,因为我省略了引用的函数:

let getLogEntries logFilePath =  
    seq {
        let logEntryLines = new ResizeArray<string>()

        for lineOfText in getLinesOfText logFilePath do                        
            if isStartOfNewLogEntry lineOfText && logEntryLines.Any() then
                yield joinLines logEntryLines
                logEntryLines.Clear()  
            logEntryLines.Add(lineOfText)  

        if logEntryLines.Any() then
            yield joinLines logEntryLines             
    }  

有更实用的方法吗?

我不能使用Seq.map因为它不是一对一的映射,而且Seq.fold似乎不正确,因为我怀疑它会在返回结果之前处理整个输入序列(如果我有非常大的日志文件,效果Seq.fold )。 我认为上面的代码不是在F#中执行此操作的理想方法,因为它使用的是ResizeArray<string>

通常,当没有可用的内置函数时,解决问题的功能方法是使用递归。 在这里,您可以递归地遍历输入,记住最后一个块的内容(因为最后一个[xyz] Info行),并在到达新的起始块时产生新的结果。 在F#中,您可以使用序列表达式很好地编写此代码:

let rec joinDetails (lines:string list) lastChunk = seq {
  match lines with
  | [] -> 
      // We are at the end - if there are any records left, produce a new item!
      if lastChunk <> [] then yield String.concat " " (List.rev lastChunk)
  | line::lines when line.StartsWith("[") ->
      // New block starting. Produce a new item and then start a new chunk
      if lastChunk <> [] then yield String.concat " " (List.rev lastChunk)
      yield! joinDetails lines [line]
  | line::lines ->
      // Ordinary line - just add it to the last chunk that we're collection
      yield! joinDetails lines (line::lastChunk) }

这是一个显示正在运行的代码的示例:

let lines = 
  [ "[20150101] error a"
    "details 1"
    "details 2"
    "[20150101] error b"
    "details"
    "[20150101] error c" ]

joinDetails lines []

Seq内置的功能不足以帮助您,因此您必须推出自己的解决方案。 最终,像这样解析文件涉及迭代和维护状态,但是F#所做的是通过计算表达式封装了该迭代和状态(因此您将使用seq计算表达式)。

您所做的事情还不错,但是您可以将代码提取到一个通用函数中,该函数在不了解格式的情况下按输入序列计算 (即字符串序列)。 剩下的部分,即解析实际的日志文件,可以使之完全起作用。

过去,我已经编写了此功能来帮助解决此问题。

let chunkBy chunkIdentifier source = 
    seq { 
        let chunk = ref []
        for sourceItem in source do
            let isNewChunk = chunkIdentifier sourceItem
            if isNewChunk && !chunk <> [] then 
                yield !chunk
                chunk := [ sourceItem ]
            else chunk := !chunk @ [ sourceItem ] 

        yield !chunk
    }

它需要一个chunkIdentifier函数,如果输入是新块的开始,则该函数返回true。

解析日志文件只是提取行,计算块并连接每个块的一种情况:

logEntryLines |> chunkBy (fun line -> line.[0] = '[')
    |> Seq.map (fun s -> String.Join (" ", s))

通过尽可能多地封装迭代和变异,同时创建可重用的函数,这更符合函数式编程的精神。

另外,还有两个变体:

let lst = ["[20150101] error a";
           "details 1";
           "details 2";
           "[20150101] error b";
           "details";
           "[20150101] error c";]

let fun1 (xs:string list) = 
    let sb = new System.Text.StringBuilder(xs.Head) 
    xs.Tail

    |> Seq.iter(fun x -> match x.[0] with
                         | '[' -> sb.Append("\n" + x) 
                         | _   -> sb.Append(" "  + x) 
                         |> ignore)
    sb.ToString()

lst  |> fun1 |> printfn "%s"

printfn "";

let fun2 (xs:string list) =  
    List.fold(fun acc (x:string) -> acc + 
                                    match x.[0] with| '[' -> "\n"  | _   -> " " 
                                    + x) xs.Head xs.Tail 

lst |> fun2 |> printfn "%s"

打印:

[20150101] error a details 1 details 2
[20150101] error b details
[20150101] error c

[20150101] error a details 1 details 2
[20150101] error b details
[20150101] error c

链接: https//dotnetfiddle.net/3KcIwv

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM