【问题标题】:Improving clojure lazy-seq usage for iterative text parsing改进用于迭代文本解析的 clojure 惰性序列
【发布时间】:2010-07-21 21:02:47
【问题描述】:

我正在编写 this coding challenge 的 Clojure 实现,试图找到 Fasta 格式的序列记录的平均长度:

>1
GATCGA
GTC
>2
GCA
>3
AAAAA

有关更多背景信息,请参阅related StackOverflow post 关于 Erlang 解决方案。

我的 Clojure 初学者尝试使用lazy-seq 尝试一次读取文件一条记录,以便将其扩展到大文件。然而,它相当消耗内存并且速度很慢,所以我怀疑它没有以最佳方式实现。这是一个使用BioJava 库来抽象解析记录的解决方案:

(import '(org.biojava.bio.seq.io SeqIOTools))
(use '[clojure.contrib.duck-streams :only (reader)])

(defn seq-lengths [seq-iter]
  "Produce a lazy collection of sequence lengths given a BioJava StreamReader"
  (lazy-seq
    (if (.hasNext seq-iter)
      (cons (.length (.nextSequence seq-iter)) (seq-lengths seq-iter)))))

(defn fasta-to-lengths [in-file seq-type]
  "Use BioJava to read a Fasta input file as a StreamReader of sequences"
  (seq-lengths (SeqIOTools/fileToBiojava "fasta" seq-type (reader in-file))))

(defn average [coll]
  (/ (reduce + coll) (count coll)))

(when *command-line-args*
  (println
    (average (apply fasta-to-lengths *command-line-args*))))

以及没有外部库的等效方法:

(use '[clojure.contrib.duck-streams :only (read-lines)])

(defn seq-lengths [lines cur-length]
  "Retrieve lengths of sequences in the file using line lengths"
  (lazy-seq
    (let [cur-line (first lines)
          remain-lines (rest lines)]
      (if (= nil cur-line) [cur-length]
        (if (= \> (first cur-line))
          (cons cur-length (seq-lengths remain-lines 0))
          (seq-lengths remain-lines (+ cur-length (.length cur-line))))))))

(defn fasta-to-lengths-bland [in-file seq-type]
  ; pop off first item since it will be everything up to the first >
  (rest (seq-lengths (read-lines in-file) 0)))

(defn average [coll]
  (/ (reduce + coll) (count coll)))

(when *command-line-args*
  (println
    (average (apply fasta-to-lengths-bland *command-line-args*))))

当前的实现需要 44 秒处理大文件,而 Python 实现需要 7 秒。您能否就加快代码速度并使其更直观提供任何建议?使用lazy-seq 是否按预期正确解析文件记录?

【问题讨论】:

    标签: clojure lazy-evaluation bioinformatics


    【解决方案1】:

    这可能无关紧要,但average 正在抓住长度序列的头部。
    以下是完全未经测试但更懒惰的方式来做我认为你想要的。

    (use 'clojure.java.io) ;' since 1.2
    
    (defn lazy-avg [coll]
      (let [f (fn [[v c] val] [(+ v val) (inc c)])
            [sum cnt] (reduce f [0 0] coll)]
        (if (zero? cnt) 0 (/ sum cnt)))
    
    (defn fasta-avg [f]
      (->> (reader f) 
        line-seq
        (filter #(not (.startsWith % ">")))
        (map #(.length %))
        lazy-avg))
    

    【讨论】:

    • 实际上我认为这很可能与大型数据集有关...我什至之前发布了一个答案指出这一点,但随后提出了一种在瞬间失灵的情况下处理问题的荒谬方法-- +1 以获得正确的解决方案。
    • 我认为 > 之间的组行应该被视为单个记录;像(partition-by #(.startsWith ^String % ">")) 这样的东西可能会有所帮助。总体思路保持不变。
    • ataggart 和 Michal,非常感谢您的指点。有了它们,这是一个更简洁的版本,可以在 1/4 的时间内完成:gist.github.com/485853。这比我的 Python 版本慢了大约 2 倍,包括从命令行启动 JVM 的时间。我从这个练习中学到了很多东西;如果还有其他明显的改进领域,请告诉我,我可以迭代另一个版本。
    • lazy-avg 懒惰而average 不是?当调用它们时,它们都会评估传递给它们的整个集合。
    • 不同之处在于平均调用计数会耗尽整个 seq,因此整个 seq 需要立即在内存中。 lazy-avg 在每次迭代时都会携带累积大小,因此在每一步之后,前面的元素都可以被垃圾收集。
    【解决方案2】:

    你的average 函数是非惰性的——它需要在抓住它的头的同时实现整个coll 参数。 更新:刚刚意识到我原来的答案包括一个关于如何解决上述问题的荒谬建议......啊。幸运的是 ataggart 已经发布了一个正确的解决方案。

    除此之外,您的代码乍一看确实很懒惰,尽管目前不鼓励使用read-lines(改用line-seq)。

    如果文件真的很大并且你的函数会被大量调用,在seq-length的参数向量中类型提示seq-iter -- ^NameOfBiojavaSeqIterClass seq-iter,使用#^代替@987654329 @ 如果您使用 Clojure 1.1 - 可能会产生重大影响。事实上,(set! *warn-on-reflection* true)然后编译您的代码并添加类型提示以删除所有反射警告。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-06-26
      • 1970-01-01
      • 2010-12-08
      • 2014-06-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多