Блоки итераторов в Clojure?

Я использую clojure.contrib.sql получить некоторые записи из базы данных SQLite.

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

Теперь я не хочу реализовывать всю последовательность перед возвращением из функции (т.е. я хочу сохранить ее ленивой), но если я вернусь res прямо или оберните его какой-нибудь ленивой оберткой (например, я хочу сделать определенный map преобразование в результирующую последовательность), привязки, связанные с SQL, будут сброшены, а соединение будет закрыто после того, как я вернусь, поэтому реализация последовательности вызовет исключение.

Как я могу заключить всю функцию в замыкание и вернуть своего рода блок итератора (например, yield в C# или Python)?

Или есть другой способ вернуть ленивую последовательность из этой функции?

4 ответа

resultset-seq тот with-query-results возврат, вероятно, уже столь же ленив, как вы собираетесь получить. Лень работает только до тех пор, пока ручка открыта, как вы сказали. Там нет никакого способа обойти это. Вы не можете читать из базы данных, если дескриптор базы данных закрыт.

Если вам нужно выполнить ввод-вывод и сохранить данные после закрытия дескриптора, то откройте дескриптор, быстро отбросьте его (победив лень), закройте дескриптор и поработайте с результатами позже. Если вы хотите перебрать некоторые данные, не сохраняя их все в памяти сразу, затем откройте дескриптор, получите ленивый seq для данных, 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 вызовите себя, и ваши преобразования данных после выборки будут ленивыми.

Фактически возможно добавить "завершающий побочный эффект" в ленивую последовательность, которая будет выполнена один раз, когда вся последовательность используется впервые:

(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

Я не уверен, что использовал бы это, чтобы закрыть соединение с БД, хотя - я думаю, что считается лучшей практикой не удерживать соединение с БД на неопределенный срок и помещать ваш вызов на закрытие соединения в конце вашей ленивой последовательности результатов. будет не только удерживать соединение дольше, чем это строго необходимо, но также откроет возможность того, что ваша программа потерпит неудачу по несвязанной причине, даже не закрыв соединение. Таким образом, для этого сценария я обычно просто хлестал бы все данные. Как говорит Брайан, вы можете хранить все это где-то необработанным, чем выполнять любые преобразования лениво, так что у вас все будет хорошо, если вы не пытаетесь собрать действительно огромный набор данных в одном блоке.

Но тогда я не знаю ваших точных обстоятельств, поэтому, если это имеет смысл с вашей точки зрения, вы определенно можете вызвать функцию закрытия соединения в хвостовой части вашей последовательности результатов. Как указывает Михил Боркент, вы не сможете использовать with-connection если ты хотел сделать это.

Нет способа создать функцию или макрос "сверху" with-connection а также with-query-results добавить лень. Оба закрывают свои Connection и ResultSet соответственно, когда поток управления покидает лексическую область.

Как сказал Михал, не составит труда создать ленивый seq, лениво закрывать 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
  ))

Это позволит, например, оставить наборы результатов открытыми, пока текущий запрос не будет завершен.

Я никогда раньше не использовал SQLite с Clojure, но я предполагаю, что with-connection закрывает соединение, когда его тело было оценено. Поэтому вам нужно самостоятельно управлять соединением, если вы хотите оставить его открытым, и закрыть его, когда закончите читать интересующие вас элементы.

Другие вопросы по тегам