这个“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)
[#{} #{} #{} #{} #{} #{} #{} #{} #{} #{} #{}]
所以我想我的问题在于 doseq
与 for
有何不同?是不同还是我的实现不正确?
doseq
与 for
的不同之处在于它用于 运行 序列上的函数,仅用于副作用。
如果您查看 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.
强制计算惰性序列
如果你一整天都在关注我的问题,
我正在用 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)
[#{} #{} #{} #{} #{} #{} #{} #{} #{} #{} #{}]
所以我想我的问题在于 doseq
与 for
有何不同?是不同还是我的实现不正确?
doseq
与 for
的不同之处在于它用于 运行 序列上的函数,仅用于副作用。
如果您查看 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.