Блоки итераторов в Clojure? - PullRequest
6 голосов
/ 02 мая 2010

Я использую 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 ]

7 голосов
/ 02 мая 2010

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 и ваши преобразования данных после выборки будут ленивыми.

3 голосов
/ 02 мая 2010

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

(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, если захотите это сделать.

0 голосов
/ 17 октября 2010

Нет способа создать функцию или макрос "сверху" из 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
  ))

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

0 голосов
/ 02 мая 2010

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

...