这个“doseq”语句和“for”语句有什么区别?在clojure中读取文件?

What is the difference between this `doseq` statement and a `for` statement; reading file in clojure?

如果你一整天都在关注我的问题,

我正在用 clojure 做一个 class 项目,但在读取文件、解析文件以及根据其内容创建图表时遇到困难。我已经设法打开并读取文件以及根据需要解析行。我现在面临的问题是根据读入的数据创建图形结构。

先介绍一些背景。在我在这个项目中实现的其他功能中,我使用 for 语句来 "build up" 值列表

...
(let [rem-list (remove nil? (for [j (range (count (graph n)))]
    (cond (< (rand) 0.5)
        [n (nth (seq (graph n)) j)])))
...

这个 for 会建立一个要从图中删除的边列表,完成后我可以在 reduce 中使用 rem-list 来删除所有边从一些图形结构。

回到我的问题。我想如果我要逐行读取文件,我可以 "build up" 以相同的方式列出一个列表,所以我实现了下面的功能

(defn readGraphFile [filename, numnodes]
  (let [edge-list 
        (with-open [rdr (io/reader filename)]
          (doseq [line (line-seq rdr)]
           (lineToEdge line)))]
    (edge-list)))

虽然如果我要 运行 这个函数,我最终会得到一个空指针异常,就好像什么都没有 "added" 到 edge-list 一样。所以是lazy/good?程序员我是我很快想到了另外一个办法。尽管它仍然在某种程度上依赖于我对 for 如何构建列表的思考。

在这个函数中,我首先 let [graph 等于一个具有已知节点数的空图。然后每次读取一行时,我只需将该边(文件中的每一行都是一条边)添加到图形中,实际上 "building up" 我的图形。功能如下图

(defn readGraph [filename, numnodes]
  (let [graph (empty-graph numnodes)]
    (with-open [rdr (io/reader filename)]
      (doseq [line (line-seq rdr)]
        (add-edge graph (lineToEdge line))))
    graph))

此处 lineToEdge returns 一对数字(例如 [1 2])。这是 add-edge 函数的正确输入。

finalproject.core> (add-edge (empty-graph 5) (lineToEdge "e 1 2"))
[#{} #{2} #{1} #{} #{}]

虽然这个函数的问题是它似乎从未真正向图形添加边

finalproject.core> (readGraph "/home/eccomp/finalproject/resources/11nodes.txt" 11)
[#{} #{} #{} #{} #{} #{} #{} #{} #{} #{} #{}]

所以我想我的问题在于 doseqfor 有何不同?是不同还是我的实现不正确?

doseqfor 的不同之处在于它用于 运行 序列上的函数,仅用于副作用。

如果您查看 doseq 的文档: (https://clojuredocs.org/clojure.core/doseq)

Repeatedly executes body (presumably for side-effects) with bindings and filtering as provided by "for". Does not retain the head of the sequence. Returns nil

因此,无论您进行何种处理,nil 都只会被 returned。

您可以将 doseq 切换为 for,应该可以。但是,line-seq 是惰性的,因此您可能需要做的是将其包装在 doall 中以确保它会在文件打开时尝试读取所有行。

此外,您的第二个 readGraph 函数只会 return 一个空图:

(defn readGraph [filename, numnodes]
  (let [graph (empty-graph numnodes)]
    (with-open [rdr (io/reader filename)]
      (doseq [line (line-seq rdr)]
        (add-edge graph (lineToEdge line))))
    graph))

最后一行只是您使用 let 设置的空图,因为 Clojure 是一种不可变的语言,图引用永远不会更新,因为您有一个函数接受现有图并添加一条边为此,您需要在传递您正在构建的列表时单步执行列表。

我知道一定有更好的方法来做到这一点,但我并不像我希望的那样擅长 Clojure,但是类似:

(defn readGraph
  [filename numnodes]
  (with-open [rdr (io/reader filename)]
    (let [edge-seq (line-seq rdr)]
        (loop [cur-line (first edge-seq)
               rem-line (rest edge-seq)
               graph (empty-graph numnodes)]
          (if-not cur-line
            graph
            (recur (first rem-line)
                   (rest rem-line)
                   (add-edge graph (lineToEdge cur-line))))))))

可能会给你一些更接近你所追求的东西。


再考虑一下,你可以试试reduce,所以:

(defn readGraph
  [filename numnodes]
  (with-open [rdr (io/reader filename)]
    (reduce add-edge (cons (empty-graph numnodes)
                           (doall (line-seq rdr))))))

Reduce 将执行一个序列,将您传入的函数应用于前两个参数,然后将其结果作为第一个参数传递给下一个调用。 cons 在那里,所以我们可以确定空图是传入的第一个参数。

您可以在 Clojure 文档中轻松找到问题的答案。

您可以在 clojuredocs.org 网站上找到所有核心功能的完整文档,或者您可以简单地 运行 (doc <function name>) 在您的 Clojure REPL 中。

doseq function documentation 是这样说的:

=> (doc doseq)
(doc doseq)
-------------------------
clojure.core/doseq
([seq-exprs & body])
Macro
  Repeatedly executes body (presumably for side-effects) with
  bindings and filtering as provided by "for".  Does not retain
  the head of the sequence. Returns nil.

换句话说,总是returns nil。所以,你可以使用它的唯一方法是产生一些副作用(例如,重复打印一些东西到你的控制台)。

这里是 for function documentation 所说的:

=> (doc for)
(doc for)
-------------------------
clojure.core/for
([seq-exprs body-expr])
Macro
  List comprehension. Takes a vector of one or more
   binding-form/collection-expr pairs, each followed by zero or more
   modifiers, and yields a lazy sequence of evaluations of expr.
   Collections are iterated in a nested fashion, rightmost fastest,
   and nested coll-exprs can refer to bindings created in prior
   binding-forms.  Supported modifiers are: :let [binding-form expr ...],
   :while test, :when test.

  (take 100 (for [x (range 100000000) y (range 1000000) :while (< y x)] [x y]))

因此,for 函数会生成一个惰性序列,您可以将其绑定到某个变量并稍后在您的代码中使用。

注意生成的序列是 lazy。这意味着在您尝试使用(或打印)它们之前不会计算此序列的元素。例如下面的函数:

(defn noop []
  (for [i (range 10)]
    (println i))
  nil)

不会打印任何东西,因为 for 循环结果没有被使用,因此也没有被计算。您可以使用 doall function.

强制计算惰性序列