Как Clojure.Spec ссылочный тип (например, атом)?

Интересно, как бы я определил функцию с параметром, которая содержит карту в атоме.

(defn do-something [a]
  (prn (vals @a)))

Это явно не работает:

(s/fdef do-something
  :args (s/cat :a map?))

Как мне указать, что a является ссылкой на карту?


person DiegoFrings    schedule 22.06.2016    source источник


Ответы (3)


Не. clojure.spec предназначен для указания структуры данных, а атомы — это состояние, а не данные. Не каждая функция обязательно должна иметь (или проверять) спецификацию.

Мой общий совет по работе с данными с отслеживанием состояния:

  • Определите свои данные
  • Определите чистые функции, которые принимают и возвращают ваши данные
  • Создайте спецификации для этих функций данных
  • Управляйте атомами, используя только эти чистые функции в как можно меньшем количестве мест

С некоторой осторожностью вы часто можете уменьшить количество функций, которые принимают или возвращают атомы, до 0 (путем закрытия атома в том месте, где он управляется), что является достойной целью.

person Alex Miller    schedule 22.06.2016
comment
Как насчет указания атома (/ref/agent) самого для ограничений? (и если бы это было возможно — а я думаю, что так и должно быть — имело бы смысл указать те несколько функций, которые принимают или возвращают атом). - person pron; 29.06.2016
comment
@pron Я думаю, что предложение Тимоти в другом ответе через валидаторы - лучший способ указать сам ссылочный тип. Я думаю, что нормально указывать функцию как принимающую #(instance? clojure.lang.IAtom %) или что-то в этом роде. Но не каждая функция обязательно нуждается в спецификации. - person Alex Miller; 29.06.2016
comment
Да, я забыл про валидаторы. Однако было бы неплохо сделать его более структурированным и идиоматичным. Я абсолютно согласен с тем, что не все нуждается в спецификации, но хорошо иметь полную выразительность спецификации, если вы хотите (в будущем может появиться больше инструментов, таких как статический анализ, которые могли бы извлечь из этого пользу). - person pron; 29.06.2016

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

Однако один из вариантов — предоставить валидатор для атома. Вы можете легко использовать partial для этого: (set-validator! my-atom (partial s/valid? :my-spec)). Теперь атом не сможет обновиться, если значение не соответствует :my-spec.

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

person Timothy Baldridge    schedule 22.06.2016
comment
Лучшие практики разработки для нетипизированных языков обычно не одобряют присвоение переменных разных типов. Такая практика часто приводит к ошибкам в системе, ухудшает читабельность и негативно влияет на производительность. - person Dylon; 29.12.2017

Вы можете использовать with-gen, пользовательский предикат и пользовательский генератор:

(require '[clojure.spec.alpha :as spec]
         '[clojure.spec.gen.alpha :as gen])

(defn memoize! [memo key distance]
  "Memoizes the distance at the given key and returns the distance"
  (swap! memo assoc key distance)
  distance)

(spec/def ::word
  (spec/and string? (complement nil?)))

(defn map-atom? [o]
  (and (instance? clojure.lang.IAtom o)
       (map? @o)))

(def map-atom-gen
  (gen/fmap
   (fn [_] (atom {}))
   (gen/int)))

(spec/def ::map-atom
  (spec/with-gen map-atom?
    (constantly map-atom-gen)))

(spec/fdef memoize!
           :args (spec/tuple ::map-atom
                             (spec/coll-of ::word :type vector? :count 2)
                             nat-int?)
           :ret nat-int?
           :fn (fn [{[memo key distance] :args, retval :ret}]
                 (= distance (@memo key) retval)))
person Dylon    schedule 28.12.2017