Clojure中的迭代器块是什么?

6

我正在使用clojure.contrib.sql从SQLite数据库中获取一些记录。

(defn read-all-foo []
  (with-connection *db*
    (with-query-results res ["select * from foo"]
       (into [] res))))

现在,我并不想在函数返回之前实现整个序列(即我希望保持懒惰),但如果我直接返回res或将其包装在某种懒惰的包装器中(例如,我想对结果序列进行某些map转换),SQL相关的绑定将被重置,并且连接将在我返回后关闭,所以实现序列将抛出异常。

我该如何将整个函数封装在闭包中并返回一种迭代器块(类似于C#或Python中的yield)?

还是有其他方法可以从这个函数返回一个懒惰的序列?

4个回答

7
with-query-results返回的resultset-seq可能已经是最懒惰的状态了。正如您所说,只要句柄处于打开状态,惰性才能发挥作用。这是无法避免的。如果数据库句柄关闭,就无法从数据库中读取数据。
如果需要在句柄关闭后进行I/O并保留数据,则应该打开句柄,快速读入数据(打败惰性),关闭句柄,然后再处理结果。如果想要迭代一些数据而不必将其全部存储在内存中,那么可以打开句柄,在数据上获取一个惰性序列,使用doseq迭代它,然后关闭句柄。
因此,如果您想对每一行执行某些操作(以获得副作用),并且在不将整个结果集读入内存的情况下丢弃结果,则可以执行以下操作:
(defn do-something-with-all-foo [f]
  (let [sql "select * from foo"]
    (with-connection *db*
      (with-query-results res [sql]
        (doseq [row res]
          (f row))))))

user> (do-something-with-all-foo println)
{:id 1}
{:id 2}
{:id 3}
nil

;; transforming the data as you go
user> (do-something-with-all-foo #(println (assoc % :bar :baz)))
{:id 1, :bar :baz}
{:id 2, :bar :baz}
{:id 3, :bar :baz}

如果您希望长期保留数据,那么最好使用上面的read-all-foo函数一次性将其全部读入(这样就打败了惰性)。如果要转换数据,则可以在获取所有结果后对其进行map。此时,您的所有数据都将保存在内存中,但map调用本身和获取后的数据转换都是惰性的。

3
事实上,可以向惰性序列添加“终止副作用”,在第一次完全消耗整个序列时执行一次:
(def s (lazy-cat (range 10) (do (println :foo) nil)))

(first s)
; => returns 0, prints out nothing

(doall (take 10 s))
; => returns (0 1 2 3 4 5 6 7 8 9), prints nothing

(last s)
; => returns 9, prints :foo

(doall s)
; => returns (0 1 2 3 4 5 6 7 8 9), prints :foo
; or rather, prints :foo if it it's the first time s has been
; consumed in full; you'll have to redefine it if you called
; (last s) earlier

我不确定是否应该使用这种方法来关闭数据库连接。最佳实践是不要无限期地保留数据库连接,将连接关闭调用放在惰性结果的末尾不仅会比必要时间长地保留连接,而且还会打开程序因为其他原因而失败而从未关闭连接的可能性。因此,在这种情况下,我通常只会一次性获取所有数据。正如Brian所说,您可以将所有数据存储在未处理的某个地方,然后按需惰性地进行任何转换,只要您不试图一次性获取非常庞大的数据集,那么您就可以安心使用。
但是,我并不了解您的具体情况,如果从您的角度来看有意义,您肯定可以在结果序列的尾部调用连接关闭函数。正如Michiel Borkent所指出的那样,如果您想这样做,您将无法使用with-connection

0

无法在with-connectionwith-query-results之上创建函数或宏来添加惰性。当控制流离开词法作用域时,两者都会关闭其Connection和ResultSet。

正如Michal所说,创建一个惰性序列并惰性地关闭其ResultSet和Connection是没有问题的。但他也说过,除非你能保证这些序列最终会完成,否则这不是一个好主意。

一个可行的解决方案可能是:

(def *deferred-resultsets*)
(defmacro with-deferred-close [&body]
  (binding [*deferred-resultsets* (atom #{})]
    (let [ret# (do ~@body)]
      ;;; close resultsets
      ret# ))
(defmacro with-deferred-results [bind-form sql & body]
  (let [resultset# (execute-query ...)]
    (swap! *deferred-resultsets* conj resultset# )
    ;;; execute body, similar to with-query-results
    ;;; but leave resultset open
  ))

这将允许例如保持结果集打开,直到当前请求完成。


0

我以前从未在Clojure中使用过SQLite,但我猜测with-connection在其体被评估时关闭连接。因此,如果您想保持连接打开状态并在完成阅读所需元素后关闭连接,则需要自己管理连接。


我想要做的是,但我希望它能够被迭代器块闭包(或实现延迟序列接口的其他形式的闭包)自动处理。 - Alex B

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接