在 F# 中拆分可变长度的字符串序列

时间:2021-08-01 14:41:45

标签: f#

我在 F# 中使用 .fasta file。当我从磁盘读取它时,它是一个字符串序列。每个观察通常是 4-5 个字符串的长度:第一个字符串是标题,然后是 2-4 个氨基酸字符串,然后是 1 个空格字符串。例如:

let filePath = @"/Users/XXX/sample_database.fasta"
let fileContents = File.ReadLines(filePath)
fileContents |> Seq.iter(fun x -> printfn  "%s" x)

产量:

enter image description here

我正在寻找一种使用 F# 中的 OOB 高阶函数将每个观察结果拆分为自己的集合的方法。我不想使用任何可变变量或 for..each 语法。我认为 Seq.chunkBySize 会起作用 -> 但大小各不相同。有 Seq.chunkByCharacter 吗?

4 个答案:

答案 0 :(得分:2)

可变变量对此完全没问题,前提是它们的可变性不会泄漏到更广泛的上下文中。为什么你不想使用它们?

但是,如果您真的想使用核心的“函数式”,那么通常的函数式方法就是通过 fold

  • 您的折叠状态将是一对“到目前为止累积的块”和“当前块”。
  • 在每一步,如果你得到一个非空字符串,你就把它附加到“当前块”。
  • 如果您得到一个空字符串,则表示当前块已结束,因此您将当前块附加到“到目前为止的块”列表并使当前块为空。
  • 这样,在折叠结束时,您最终会得到一对“除最后一个以外的所有块”和“最后一个块”,您可以将它们粘合在一起。
  • 另外,还有一个优化细节:因为我要做很多“将一个东西附加到一个列表”,我想为此使用一个链表,因为它具有恒定时间附加。但问题是前置只是恒定时间,而不是附加,这意味着我最终会颠倒所有列表。但没关系:我会在最后再次反转它们。列表反转是一个线性操作,这意味着我的整个事情仍然是线性的。
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)

答案 1 :(得分:0)

我使用递归:

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

答案 2 :(得分:0)

您也可以将正则表达式用于此类内容。这是一个使用正则表达式一次性提取整个 Fasta Block 的解决方案。

type FastaEntry = {
    Description: string
    Sequence:    string
}

let fastaRegexStr = 
    @"
    ^>                  # Line Starting with >
      (.*)                 # Capture into $1
    \r?\n               # End-of-Line
    (                   # Capturing in $2
        (?:                   
            ^           # 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}
]

答案 3 :(得分:0)

为了说明 Fyodor's 关于包含可变性的观点,这里有一个例子,它是可变的,但仍然有些合理。外层功能层是一个序列表达式,是F# source中的Seq.scan演示的常见模式。

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";}]