Создание ленивого, нечистого генератора id

Я хотел бы знать, как создать бесконечную, нечистую последовательность уникальных значений в Clojure.

(def generator ...) ; def, not defn
(take 4 generator) ; => (1 2 3 4)
(take 4 generator) ; => (5 6 7 8). note the generator impurity.

Я думаю, что такой дизайн может быть более удобным, чем, например, обертывание единственного целочисленного значения в ссылочный тип и увеличение его от его потребителей, например:

  • Предлагаемый подход уменьшает детали реализации до одной точки изменения: генератора. В противном случае все потребители должны были заботиться как о ссылочном типе (atom), так и о конкретной функции, которая предоставляет следующее значение (inc)
  • Последовательности могут воспользоваться многими функциями Clojure.core. "Вручную" создание списка идентификаторов из атома будет немного громоздким: (take 4 (repeatedly #(swap! _ inc)))

Я не мог придумать рабочую реализацию. Возможно ли вообще?

Ответ 1

Вы можете обернуть ленивую последовательность вокруг нечистого класса (например, java.util.concurrent.atomic.AtomicLong), чтобы создать последовательность идентификаторов:

(def id-counter (java.util.concurrent.atomic.AtomicLong.))

(defn id-gen []
  (cons
   (.getAndIncrement id-counter)
   (lazy-seq
     (id-gen))))

Это работает, но только если вы не сохранили главу последовательности. Если вы создаете var, который захватывает голову:

(def id-seq (id-gen))

Затем назовите его повторно, он вернет идентификаторы от начала последовательности, потому что вы удержали главу последовательности:

(take 3 id-seq)
;; => (0 1 2)
(take 3 id-seq)
;; => (0 1 2)
(take 3 id-seq)
;; => (0 1 2)

Если вы заново создаете последовательность, вы получите свежие значения из-за примеси:

(take 3 (id-gen))
;; (3 4 5)
(take 3 (id-gen))
;; (6 7 8)
(take 3 (id-gen))
;; (9 10 11)

Я рекомендую сделать следующее только для образовательных целей (а не для производственного кода), но вы можете создать свой собственный экземпляр ISeq, который непосредственно использует примесь:

(def custom-seq
     (reify clojure.lang.ISeq
            (first [this] (.getAndIncrement id-counter))
            (next  [this] (.getAndIncrement id-counter))
            (cons  [this thing]
                   (cons thing this))
            (more [this] (cons
                          (.getAndIncrement id-counter)
                          this))
            (count [this] (throw (RuntimeException. "count: not supported")))
            (empty [this] (throw (RuntimeException. "empty: not supported")))
            (equiv [this obj] (throw (RuntimeException. "equiv: not supported")))
            (seq   [this] this)))

(take 3 custom-seq)
;; (12 13 14)
(take 3 custom-seq)
;; (15 16 17)

Ответ 2

Мне было весело, когда я что-то обнаружил во время ответа на ваш вопрос. Первое, что произошло со мной, было то, что, возможно, для какой бы конечной цели вам не нужны эти идентификаторы, функция gensym может быть полезна.

Затем я подумал: "Хорошо, эй, кажется, что он увеличивает некоторый нечистый счетчик для создания новых идентификаторов" и "хорошо, эй, что в исходном коде для этого?" Что привело меня к этому:

(. clojure.lang.RT (nextID))

Что, кажется, делает то, что вам нужно. Круто! Если вы хотите использовать его так, как вы предлагаете, я бы, вероятно, сделал бы это функцией:

(defn generate-id []
  (. clojure.lang.RT (nextID)))

Затем вы можете сделать:

user> (repeatedly 5 generate-id)
=> (372 373 374 375 376)

Я еще не тестировал, будет ли это производить всегда уникальные значения "глобально" - я не уверен в терминологии, но я говорю о том, когда вы можете использовать эту функцию generate-id из разных потоков, но хотите по-прежнему убедиться, что он создает уникальные значения.

Ответ 3

это другое решение, возможно:

user=> (defn positive-numbers
          ([] (positive-numbers 1))
          ([n] (cons n (lazy-seq (positive-numbers (inc n))))))
#'user/positive-numbers
user=> (take 4 (positive-numbers))
(1 2 3 4)
user=> (take 4 (positive-numbers 5))
(5 6 7 8)

Ответ 4

Способ, который был бы более идиоматичным, потокобезопасным и приглашал бы никакую странность в отношении ссылок на головы, заключался бы в том, чтобы использовать закрытие над одним из clojures, встроенным в изменяемые ссылки. Вот небольшой пример, с которым я работал, так как у меня была такая же проблема. Он просто закрывается над ссылкой.

(def id-generator (let [counter (ref 0)]
                (fn [] (dosync (let [cur-val @counter] 
                         (do (alter counter + 1)
                           cur-val))))))

Каждый раз, когда вы вызываете (идентификатор-генератор), вы получите следующее число в последовательности.

Ответ 5

Вот еще один быстрый способ:

user> (defn make-generator [& [ii  init]]
  (let [a (atom (or ii 0 ))
        f #(swap! a inc)]
    #(repeatedly f)))
#'user/make-generator
user> (def g (make-generator))
#'user/g
user> (take 3 (g))
(1 2 3)
user> (take 3 (g))
(4 5 6)
user> (take 3 (g))
(7 8 9)

Ответ 6

Это взломать, но он работает, и это очень просто

; there be dragons !
(defn id-gen [n] (repeatedly n (fn [] (hash #()))))
(id-gen 3) ; (2133991908 877609209 1060288067 442239263 274390974)

В принципе clojure создает "анонимную" функцию, но поскольку для имени clojure требуется имя для этого, он использует uniques impure ids, чтобы избежать коллизий. Если у вас есть уникальное имя, вы должны получить уникальный номер.

Надеюсь, что это поможет

Ответ 7

Создание идентификаторов из произвольного набора идентификаторов семян:

(defonce ^:private counter (volatile! 0))

(defn- next-int []
  (vswap! counter inc))

(defn- char-range
  [a b]
  (mapv char
        (range (int a) (int b))))

(defn- unique-id-gen
  "Generates a sequence of unique identifiers seeded with ids sequence"
  [ids]
  ;; Laziness ftw:
  (apply concat
         (iterate (fn [xs]
                    (for [x xs
                          y ids]
                      (str x y)))
                  (map str ids))))

(def inf-ids-seq (unique-id-gen (concat (char-range \a \z)
                                        (char-range \A \Z)
                                        (char-range \0 \9)
                                        [\_ \-])))

(defn- new-class
  "Returns an unused new classname"
  []
  (nth inf-ids-seq (next-int)))

(repeatedly 10 new-class)

Демонстрация:

(take 16 (unique-id-gen [\a 8 \c]))
;; => ("a" "8" "c" "aa" "a8" "ac" "8a" "88" "8c" "ca" "c8" "cc" "aaa" "aa8" "aac" "a8a")