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