在 F# 中拆分可变长度的字符串序列
Splitting a Seq of Strings Of Variable Length in F#
我在 F# 中使用 .fasta file。当我从磁盘读取它时,它是一个字符串序列。每个观察的长度通常为 4-5 个字符串:第一个字符串是标题,然后是 2-4 个氨基酸字符串,然后是 space 的 1 个字符串。例如:
let filePath = @"/Users/XXX/sample_database.fasta"
let fileContents = File.ReadLines(filePath)
fileContents |> Seq.iter(fun x -> printfn "%s" x)
产量:
我正在寻找一种使用 F# 中的 OOB 高阶函数将每个观察拆分为自己的 collection 的方法。我不想使用任何可变变量或 for..each 语法。我认为 Seq.chunkBySize 可以工作 -> 但大小不同。有没有Seq.chunkByCharacter?
可变变量对此完全没问题,前提是它们的可变性不会泄漏到更广泛的上下文中。为什么您不想使用它们?
但是如果你真的想要硬核“函数式”,那么通常的函数式方法是通过 fold
.
- 您的折叠状态将是一对“到目前为止累积的区块”和“当前区块”。
- 在每一步,如果你得到一个非空字符串,你将它附加到“当前块”。
- 如果你得到一个空字符串,这意味着当前块已经结束,所以你将当前块附加到“到目前为止的块”列表并使当前块为空。
- 这样,在折叠结束时,您将得到一对“除了最后一个以外的所有积木”和“最后一个积木”,您可以将它们粘在一起。
- 另外,一个优化细节:因为我要做很多“将一个东西附加到一个列表”,我想为此使用一个链表,因为它具有恒定时间附加。但接下来的问题是,prepending 只是恒定时间,而不是 appending,这意味着我最终会颠倒所有列表。但没关系:我会在最后再次反转它们。列表反转是一个线性操作,这意味着我的整个事情仍然是线性的。
let splitEm lines =
let step (blocks, currentBlock) s =
match s with
| "" -> (List.rev currentBlock :: blocks), []
| _ -> blocks, s :: currentBlock
let (blocks, lastBlock) = Array.fold step ([], []) lines
List.rev (lastBlock :: blocks)
用法:
> splitEm [| "foo"; "bar"; "baz"; ""; "1"; "2"; ""; "4"; "5"; "6"; "7"; ""; "8" |]
[["foo"; "bar"; "baz"]; ["1"; "2"]; ["4"; "5"; "6"; "7"]; ["8"]]
注 1:您可能必须根据您的数据和您希望的行为来解决一些边缘情况。例如,如果最后有一个空行,那么最后会出现一个空块。
注释 2:您可能会注意到这与具有可变变量的命令式算法非常相似:我什至在谈论诸如“附加到块列表”和“使当前块为空”。这不是巧合。在这个纯函数版本中,“变异”是通过使用不同的参数再次调用相同的函数来完成的,而在等效的命令式版本中,您只需将这些参数变成可变的存储单元。同样的事情,不同的看法。通常,任何命令式迭代都可以通过这种方式变成 fold
。
为了进行比较,下面是对上述命令式基于突变的样式的机械翻译:
let splitEm lines =
let mutable blocks = []
let mutable currentBlock = []
for s in lines do
match s with
| "" -> blocks <- List.rev currentBlock :: blocks; currentBlock <- []
| _ -> currentBlock <- s :: currentBlock
List.rev (currentBlock :: blocks)
我用了递归:
type FastaEntry = {Description:String; Sequence:String}
let generateFastaEntry (chunk:String seq) =
match chunk |> Seq.length with
| 0 -> None
| _ ->
let description = chunk |> Seq.head
let sequence = chunk |> Seq.tail |> Seq.reduce (fun acc x -> acc + x)
Some {Description=description; Sequence=sequence}
let rec chunk acc contents =
let index = contents |> Seq.tryFindIndex(fun x -> String.IsNullOrEmpty(x))
match index with
| None ->
let fastaEntry = generateFastaEntry contents
match fastaEntry with
| Some x -> Seq.append acc [x]
| None -> acc
| Some x ->
let currentChunk = contents |> Seq.take x
let fastaEntry = generateFastaEntry currentChunk
match fastaEntry with
| None -> acc
| Some y ->
let updatedAcc =
match Seq.isEmpty acc with
| true -> seq {y}
| false -> Seq.append acc (seq {y})
let remaining = contents |> Seq.skip (x+1)
chunk updatedAcc remaining
您也可以对这类内容使用正则表达式。这是一个使用正则表达式一次提取整个 Fasta 块的解决方案。
type FastaEntry = {
Description: string
Sequence: string
}
let fastaRegexStr =
@"
^> # Line Starting with >
(.*) # Capture into
\r?\n # End-of-Line
( # Capturing in
(?:
^ # A Line ...
[A-Z]+ # .. containing A-Z
\*? \r?\n # Optional(*) followed by End-of-Line
)+ # ^ Multiple of those lines
)
(?:
(?: ^ [ \t\v\f]* \r?\n ) # Match an empty (whitespace) line ..
| # or
\z # End-of-String
)
"
(* Regex for matching one Fasta Block *)
let fasta = Regex(fastaRegexStr, RegexOptions.IgnorePatternWhitespace ||| RegexOptions.Multiline)
(* Whole file as a string *)
let content = System.IO.File.ReadAllText "fasta.fasta"
let entries = [
for m in fasta.Matches(content) do
let desc = m.Groups.[1].Value
(* Remove *, \r and \n from string *)
let sequ = Regex.Replace(m.Groups.[2].Value, @"\*|\r|\n", "")
{Description=desc; Sequence=sequ}
]
为了说明 point about contained mutability, here's an example that is mutable as can be while still somewhat reasonable. The outer functional layer is a sequence expression, a common pattern demonstrated by Seq.scan
in the F# source.
let chooseFoldSplit
folding (state : 'State)
(source : seq<'T>) : seq<'U[]> = seq {
let sref, zs = ref state, ResizeArray()
use ie = source.GetEnumerator()
while ie.MoveNext() do
let newState, uopt = folding !sref ie.Current
if newState <> !sref then
yield zs.ToArray()
zs.Clear()
sref := newState
match uopt with
| None -> ()
| Some u -> zs.Add u
if zs.Count > 0 then
yield zs.ToArray() }
// val chooseFoldSplit :
// folding:('State -> 'T -> 'State * 'U option) ->
// state:'State -> source:seq<'T> -> seq<'U []> when 'State : equality
ref cell有可变性(相当于可变变量),有可变数据结构; System.Collection.Generic.List<'T>
的别名,允许以 O(1) 成本追加。
折叠函数的签名'State -> 'T -> 'State * 'U option
让人想起fold
的文件夹,除了它会导致结果序列在其状态改变时被拆分。它还会产生一个选项,表示当前组的下一个成员(或不是)。
即使不转换为持久数组也能正常工作,只要您惰性地迭代生成的序列并且仅恰好一次。因此我们需要将 ResizeArray
的内容与外界隔离。
对于您的用例,最简单的折叠是布尔值的否定,但您可以将其用于更复杂的任务,例如为记录编号:
[| "foo"; "1"; "2"; ""; "bar"; "4"; "5"; "6"; "7"; ""; "baz"; "8"; "" |]
|> chooseFoldSplit (fun b t ->
if t = "" then not b, None else b, Some t ) false
|> Seq.map (fun a ->
if a.Length > 1 then
{ Description = a.[0]; Sequence = String.concat "" a.[1..] }
else failwith "Format error" )
// val it : seq<FastaEntry> =
// seq [{Description = "foo";
// Sequence = "12";}; {Description = "bar";
// Sequence = "4567";}; {Description = "baz";
// Sequence = "8";}]
我在 F# 中使用 .fasta file。当我从磁盘读取它时,它是一个字符串序列。每个观察的长度通常为 4-5 个字符串:第一个字符串是标题,然后是 2-4 个氨基酸字符串,然后是 space 的 1 个字符串。例如:
let filePath = @"/Users/XXX/sample_database.fasta"
let fileContents = File.ReadLines(filePath)
fileContents |> Seq.iter(fun x -> printfn "%s" x)
产量:
我正在寻找一种使用 F# 中的 OOB 高阶函数将每个观察拆分为自己的 collection 的方法。我不想使用任何可变变量或 for..each 语法。我认为 Seq.chunkBySize 可以工作 -> 但大小不同。有没有Seq.chunkByCharacter?
可变变量对此完全没问题,前提是它们的可变性不会泄漏到更广泛的上下文中。为什么您不想使用它们?
但是如果你真的想要硬核“函数式”,那么通常的函数式方法是通过 fold
.
- 您的折叠状态将是一对“到目前为止累积的区块”和“当前区块”。
- 在每一步,如果你得到一个非空字符串,你将它附加到“当前块”。
- 如果你得到一个空字符串,这意味着当前块已经结束,所以你将当前块附加到“到目前为止的块”列表并使当前块为空。
- 这样,在折叠结束时,您将得到一对“除了最后一个以外的所有积木”和“最后一个积木”,您可以将它们粘在一起。
- 另外,一个优化细节:因为我要做很多“将一个东西附加到一个列表”,我想为此使用一个链表,因为它具有恒定时间附加。但接下来的问题是,prepending 只是恒定时间,而不是 appending,这意味着我最终会颠倒所有列表。但没关系:我会在最后再次反转它们。列表反转是一个线性操作,这意味着我的整个事情仍然是线性的。
let splitEm lines =
let step (blocks, currentBlock) s =
match s with
| "" -> (List.rev currentBlock :: blocks), []
| _ -> blocks, s :: currentBlock
let (blocks, lastBlock) = Array.fold step ([], []) lines
List.rev (lastBlock :: blocks)
用法:
> splitEm [| "foo"; "bar"; "baz"; ""; "1"; "2"; ""; "4"; "5"; "6"; "7"; ""; "8" |]
[["foo"; "bar"; "baz"]; ["1"; "2"]; ["4"; "5"; "6"; "7"]; ["8"]]
注 1:您可能必须根据您的数据和您希望的行为来解决一些边缘情况。例如,如果最后有一个空行,那么最后会出现一个空块。
注释 2:您可能会注意到这与具有可变变量的命令式算法非常相似:我什至在谈论诸如“附加到块列表”和“使当前块为空”。这不是巧合。在这个纯函数版本中,“变异”是通过使用不同的参数再次调用相同的函数来完成的,而在等效的命令式版本中,您只需将这些参数变成可变的存储单元。同样的事情,不同的看法。通常,任何命令式迭代都可以通过这种方式变成 fold
。
为了进行比较,下面是对上述命令式基于突变的样式的机械翻译:
let splitEm lines =
let mutable blocks = []
let mutable currentBlock = []
for s in lines do
match s with
| "" -> blocks <- List.rev currentBlock :: blocks; currentBlock <- []
| _ -> currentBlock <- s :: currentBlock
List.rev (currentBlock :: blocks)
我用了递归:
type FastaEntry = {Description:String; Sequence:String}
let generateFastaEntry (chunk:String seq) =
match chunk |> Seq.length with
| 0 -> None
| _ ->
let description = chunk |> Seq.head
let sequence = chunk |> Seq.tail |> Seq.reduce (fun acc x -> acc + x)
Some {Description=description; Sequence=sequence}
let rec chunk acc contents =
let index = contents |> Seq.tryFindIndex(fun x -> String.IsNullOrEmpty(x))
match index with
| None ->
let fastaEntry = generateFastaEntry contents
match fastaEntry with
| Some x -> Seq.append acc [x]
| None -> acc
| Some x ->
let currentChunk = contents |> Seq.take x
let fastaEntry = generateFastaEntry currentChunk
match fastaEntry with
| None -> acc
| Some y ->
let updatedAcc =
match Seq.isEmpty acc with
| true -> seq {y}
| false -> Seq.append acc (seq {y})
let remaining = contents |> Seq.skip (x+1)
chunk updatedAcc remaining
您也可以对这类内容使用正则表达式。这是一个使用正则表达式一次提取整个 Fasta 块的解决方案。
type FastaEntry = {
Description: string
Sequence: string
}
let fastaRegexStr =
@"
^> # Line Starting with >
(.*) # Capture into
\r?\n # End-of-Line
( # Capturing in
(?:
^ # A Line ...
[A-Z]+ # .. containing A-Z
\*? \r?\n # Optional(*) followed by End-of-Line
)+ # ^ Multiple of those lines
)
(?:
(?: ^ [ \t\v\f]* \r?\n ) # Match an empty (whitespace) line ..
| # or
\z # End-of-String
)
"
(* Regex for matching one Fasta Block *)
let fasta = Regex(fastaRegexStr, RegexOptions.IgnorePatternWhitespace ||| RegexOptions.Multiline)
(* Whole file as a string *)
let content = System.IO.File.ReadAllText "fasta.fasta"
let entries = [
for m in fasta.Matches(content) do
let desc = m.Groups.[1].Value
(* Remove *, \r and \n from string *)
let sequ = Regex.Replace(m.Groups.[2].Value, @"\*|\r|\n", "")
{Description=desc; Sequence=sequ}
]
为了说明Seq.scan
in the F# source.
let chooseFoldSplit
folding (state : 'State)
(source : seq<'T>) : seq<'U[]> = seq {
let sref, zs = ref state, ResizeArray()
use ie = source.GetEnumerator()
while ie.MoveNext() do
let newState, uopt = folding !sref ie.Current
if newState <> !sref then
yield zs.ToArray()
zs.Clear()
sref := newState
match uopt with
| None -> ()
| Some u -> zs.Add u
if zs.Count > 0 then
yield zs.ToArray() }
// val chooseFoldSplit :
// folding:('State -> 'T -> 'State * 'U option) ->
// state:'State -> source:seq<'T> -> seq<'U []> when 'State : equality
ref cell有可变性(相当于可变变量),有可变数据结构; System.Collection.Generic.List<'T>
的别名,允许以 O(1) 成本追加。
折叠函数的签名'State -> 'T -> 'State * 'U option
让人想起fold
的文件夹,除了它会导致结果序列在其状态改变时被拆分。它还会产生一个选项,表示当前组的下一个成员(或不是)。
即使不转换为持久数组也能正常工作,只要您惰性地迭代生成的序列并且仅恰好一次。因此我们需要将 ResizeArray
的内容与外界隔离。
对于您的用例,最简单的折叠是布尔值的否定,但您可以将其用于更复杂的任务,例如为记录编号:
[| "foo"; "1"; "2"; ""; "bar"; "4"; "5"; "6"; "7"; ""; "baz"; "8"; "" |]
|> chooseFoldSplit (fun b t ->
if t = "" then not b, None else b, Some t ) false
|> Seq.map (fun a ->
if a.Length > 1 then
{ Description = a.[0]; Sequence = String.concat "" a.[1..] }
else failwith "Format error" )
// val it : seq<FastaEntry> =
// seq [{Description = "foo";
// Sequence = "12";}; {Description = "bar";
// Sequence = "4567";}; {Description = "baz";
// Sequence = "8";}]