Помогите мне сделать этот код нуба более идиоматичным - PullRequest
2 голосов
/ 31 марта 2011

Итак, я использую congomongo (функция извлечения в конце), чтобы извлечь некоторые документы из коллекции mongodb. Я хочу передать параметры вызову fetch, чтобы я мог сделать что-то вроде (posts :limit 1) и получить {:limit 1} для получения. Я делаю "памятку" с ручным переключением с @posts, потому что я хочу иметь возможность сбросить кэш, что, на мой взгляд, не может быть сделано с clojure.core/memoize.

Теперь проблема, которую я вижу здесь, состоит в том, что вызов (fetch :posts options) не является тривиальным, и я действительно предпочел бы не забивать свое хранилище данных, если dosync должен повторить транзакцию. Я - полный clojure / fp noob, хотя, и я не уверен, как обойти эту проблему. Кроме того, поскольку я новичок, если я делаю здесь что-нибудь еще, что заставляет вас съеживаться, я бы хотел узнать, как правильно написать это.

(def posts (ref nil))
(defn reset-posts [] (dosync alter posts nil))

(defn fetch-posts [& options]
  (let [options (apply array-map options)]
    (or @posts
        (dosync alter posts (fetch :posts options)))))

Ответы [ 3 ]

6 голосов
/ 31 марта 2011

Я не уверен, что ваши блоки транзакций ((dosync alter ...) делают то, что вы думаете!

user=> (def posts (ref nil))
#'user/posts
user=> (dosync (ref-set posts [1 2 3 4 5]))
[1 2 3 4 5]
user=> @posts
[1 2 3 4 5]
user=> (dosync alter posts nil)
nil
user=> @posts
[1 2 3 4 5]

В reset-posts вы, вероятно, захотите (dosync (ref-set posts nil)), а в fetch-posts исправление синтаксиса будет (dosync (ref-set posts (fetch :posts options))).

Однако в fetch-posts есть условие гонки, проверка-то-действие. Не может быть такой большой сделки; Я не уверен, кто использует fetch-posts, но перемещение бита or @posts внутри транзакции позволило бы избежать ситуации, когда обе параллельные транзакции в конечном итоге совершают изменение.

Что касается попыток fetch-posts, да, это может произойти, хотя ваше решение для кеша избегает большинства из них. Я не уверен, что есть способ обойтись без блокировки, хотя. Обычно с операциями ввода-вывода в транзакциях вы отправляете их агенту, но успех транзакции зависит от возвращаемого значения из fetch, поэтому мне не ясно, как это будет работать.

1 голос
/ 01 апреля 2011

Другим подходом, который может быть полезен для перемещения обширных вычислений за пределы dosync, является использование delay.

(defn fetch-posts
  [& options]
  @(dosync (or @posts (ref-set posts (delay (apply fetch :posts options))))))

Также обратите внимание, что ваш исходный код не поточно-ориентированный, поскольку вы обращаетесь к ссылке за пределами dosync и изменяете ее на основе этого значения впоследствии в dosync. Но значение могло уже измениться между deref и dosync. Например. другим потоком, вызывающим fetch-posts параллельно.

Также агентный подход сомнителен, потому что вы не можете надежно прочитать агент. Полученное вами значение соответствует, но доступ не синхронизирован. Рассмотрим пример Лорана: между await-for и deref другой поток может уже вызывать reset-posts, и вместо почтовых данных вы получите nil. В этом примере это, вероятно, a) надуманный и b) возможно, случай, который нужно рассмотреть в любом случае, но могут быть и другие случаи использования, когда это вводит условие тонкой гонки в более критический код.

tl; dr: Будьте осторожны, что вы делаете! Clojure не магически потокобезопасен. Тщательно продумайте свое решение и помните о последствиях.

1 голос
/ 01 апреля 2011

Итак, вы вводите ссылку, потому что вы хотите, чтобы не было возможности взорвать память, когда прошло время, потому что простое использование памятки вокруг записей извлечения может привести к этому, рано или поздно, верно?

Может быть, вы могли бы попробовать альтернативный подход: пусть fetch-посты будут "чистыми", не запоминающими. В этом сценарии кто-то может вызывать посты выборки вслепую, не опасаясь OutOfMemoryExceptions. Действительно, возможно, для некоторых случаев использования может быть достаточно «кэшировать значение» в локальном коде вызова.

Но на этом история не заканчивается, иначе я бы не стал отвечать :-): вы можете довольно легко запоминать свои «локализованные во времени» заметки, перепривязывая fetch-сообщения с помощью clojure.core / binding: from тогда весь код в том же потоке в стеке вызовов получит выгоду от связанных записанных записей извлечения. Если вы используете альфа-версию clojure 1.3, вам необходимо объявить переменную fetch-posts явно как повторно привязываемую через метаданные: dynamic.

;; most simple definition
(defn ^:dynamic fetch-posts [& options]
  (let [options (apply array-map options)]
    (fetch :posts options)))

;; a la carte caching by the calling code (lexically scoped)
(let [posts (apply fetch-posts options)] ...)

;; a la carte caching by the calling code (dynamically scoped)
(binding [fetch-posts (memoize fetch-posts)] ...)

Моим последним предположением было бы то, что вы захотите «помнить» в постах, в вашей исходной версии, путем индексации постов по ключу, который будет опцией seq, верно? Некоторые, может быть, ваш код был не прав? (или вы предполагали, что fetch-posts будут вызываться с одинаковыми аргументами снова и снова?)

Другая идея. Используйте агент для сериализации доступа на запись к сообщениям, а затем убедитесь, что вызов выборки выполняется только тогда, когда он равен nil:

    (def posts (agent nil))

    (defn reset-posts [] (send posts (constantly nil)))

    (defn fetch-posts [& options]
      (let [options (apply array-map options)]
        (send-off posts #(or % (fetch :posts options)))
        (await-for (Long/MAX_VALUE) posts)
        @posts))
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...