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
似乎不正确,因为我怀疑它会在返回结果之前处理整个输入序列(如果我有非常大的日志文件,那就不太好了)。我假设我上面的代码不是在 F# 中执行此操作的理想方法,因为它使用 ResizeArray<string>
.
Seq
中没有太多可以帮助您的内置解决方案,因此您必须推出自己的解决方案。最终,像这样解析文件涉及迭代和维护状态,但 F# 所做的是通过计算表达式封装该迭代和状态(因此您使用 seq
计算表达式)。
你所做的还不错,但你可以将你的代码提取到一个通用函数中,该函数在不知情的情况下计算输入序列中的 chunks(即字符串序列)的格式。其余的,即解析一个实际的日志文件,可以做成纯粹的功能。
我过去写过这个函数来帮助解决这个问题。
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
函数,如果输入是新块的开始,该函数 returns 为真。
解析日志文件只是提取行、计算块并连接每个块的情况:
logEntryLines |> chunkBy (fun line -> line.[0] = '[')
|> Seq.map (fun s -> String.Join (" ", s))
通过尽可能地封装迭代和变异,同时创建一个可重用的函数,更符合函数式编程的精神。
一般来说,当没有可以使用的内置函数时,函数式的解决方法就是使用递归。在这里,您可以递归遍历输入,记住最后一个块的项目(自最后 [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 []
或者,另外两个变体:
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
我有一个这样的字符串序列(文件中的行)
[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
似乎不正确,因为我怀疑它会在返回结果之前处理整个输入序列(如果我有非常大的日志文件,那就不太好了)。我假设我上面的代码不是在 F# 中执行此操作的理想方法,因为它使用 ResizeArray<string>
.
Seq
中没有太多可以帮助您的内置解决方案,因此您必须推出自己的解决方案。最终,像这样解析文件涉及迭代和维护状态,但 F# 所做的是通过计算表达式封装该迭代和状态(因此您使用 seq
计算表达式)。
你所做的还不错,但你可以将你的代码提取到一个通用函数中,该函数在不知情的情况下计算输入序列中的 chunks(即字符串序列)的格式。其余的,即解析一个实际的日志文件,可以做成纯粹的功能。
我过去写过这个函数来帮助解决这个问题。
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
函数,如果输入是新块的开始,该函数 returns 为真。
解析日志文件只是提取行、计算块并连接每个块的情况:
logEntryLines |> chunkBy (fun line -> line.[0] = '[')
|> Seq.map (fun s -> String.Join (" ", s))
通过尽可能地封装迭代和变异,同时创建一个可重用的函数,更符合函数式编程的精神。
一般来说,当没有可以使用的内置函数时,函数式的解决方法就是使用递归。在这里,您可以递归遍历输入,记住最后一个块的项目(自最后 [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 []
或者,另外两个变体:
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