Как я могу использовать свои спецификации по прямому назначению, если они находятся в отдельном пространстве имен?

Один из примеров в clojure.spec Руководстве – это простая спецификация анализа параметров:

(require '[clojure.spec :as s])

(s/def ::config
  (s/* (s/cat :prop string?
              :val (s/alt :s string? :b boolean?))))

(s/conform ::config ["-server" "foo" "-verbose" true "-user" "joe"])
;;=> [{:prop "-server", :val [:s "foo"]}
;;    {:prop "-verbose", :val [:b true]}
;;    {:prop "-user", :val [:s "joe"]}]

Позже, в разделе валидация, определяется функция, которая внутренне conform использует эту спецификацию:

(defn- set-config [prop val]
  (println "set" prop val))

(defn configure [input]
  (let [parsed (s/conform ::config input)]
    (if (= parsed ::s/invalid)
      (throw (ex-info "Invalid input" (s/explain-data ::config input)))
      (doseq [{prop :prop [_ val] :val} parsed]
        (set-config (subs prop 1) val)))))

(configure ["-server" "foo" "-verbose" true "-user" "joe"])
;; set server foo
;; set verbose true
;; set user joe
;;=> nil

Поскольку за руководством легко следить из REPL, весь этот код оценивается в одном и том же пространстве имен. Однако в этом ответе @levand рекомендует размещать спецификации в отдельных пространствах имен:

Обычно я помещаю спецификации в их собственное пространство имен рядом с пространством имен, которое они описывают.

Это нарушило бы использование ::config выше, но эту проблему можно решить:

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

(ns my.app.foo.specs
  (:require [my.app.foo :as f]))

(s/def ::f/name string?)

Далее он объясняет, что спецификации и реализации можно поместить в одно и то же пространство имен, но это не идеально:

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

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

boot.properties:

BOOT_CLOJURE_VERSION=1.9.0-alpha7

src/example/core.clj:

(ns example.core
  (:require [clojure.spec :as s]))

(defn- set-config [prop val]
  (println "set" prop val))

(defn configure [input]
  (let [parsed (s/conform ::config input)]
    (if (= parsed ::s/invalid)
      (throw (ex-info "Invalid input" (s/explain-data ::config input)))
      (doseq [{prop :prop [_ val] :val} parsed]
        (set-config (subs prop 1) val)))))

src/example/spec.clj:

(ns example.spec
  (:require [clojure.spec :as s]
            [example.core :as core]))

(s/def ::core/config
  (s/* (s/cat :prop string?
              :val (s/alt :s string? :b boolean?))))

build.boot:

(set-env! :source-paths #{"src"})

(require '[example.core :as core])

(deftask run []
  (with-pass-thru _
    (core/configure ["-server" "foo" "-verbose" true "-user" "joe"])))

Но, конечно, когда я действительно запускаю это, я получаю сообщение об ошибке:

$ boot run
clojure.lang.ExceptionInfo: Unable to resolve spec: :example.core/config

Я мог бы решить эту проблему, добавив (require 'example.spec) к build.boot, но это уродливо и чревато ошибками, и их будет только больше по мере увеличения количества пространств имен спецификаций. Я не могу require выделить пространство имен спецификации из пространства имен реализации по нескольким причинам. Вот пример использования fdef.

boot.properties:

BOOT_CLOJURE_VERSION=1.9.0-alpha7

src/example/spec.clj:

(ns example.spec
  (:require [clojure.spec :as s]))

(alias 'core 'example.core)

(s/fdef core/divisible?
  :args (s/cat :x integer? :y (s/and integer? (complement zero?)))
  :ret boolean?)

(s/fdef core/prime?
  :args (s/cat :x integer?)
  :ret boolean?)

(s/fdef core/factor
  :args (s/cat :x (s/and integer? pos?))
  :ret (s/map-of (s/and integer? core/prime?) (s/and integer? pos?))
  :fn #(== (-> % :args :x) (apply * (for [[a b] (:ret %)] (Math/pow a b)))))

src/example/core.clj:

(ns example.core
  (:require [example.spec]))

(defn divisible? [x y]
  (zero? (rem x y)))

(defn prime? [x]
  (and (< 1 x)
       (not-any? (partial divisible? x)
                 (range 2 (inc (Math/floor (Math/sqrt x)))))))

(defn factor [x]
  (loop [x x y 2 factors {}]
    (let [add #(update factors % (fnil inc 0))]
      (cond
        (< x 2) factors
        (< x (* y y)) (add x)
        (divisible? x y) (recur (/ x y) y (add y))
        :else (recur x (inc y) factors)))))

build.boot:

(set-env!
 :source-paths #{"src"}
 :dependencies '[[org.clojure/test.check "0.9.0" :scope "test"]])

(require '[clojure.spec.test :as stest]
         '[example.core :as core])

(deftask run []
  (with-pass-thru _
    (prn (stest/run-all-tests))))

Первая проблема наиболее очевидна:

$ boot run
clojure.lang.ExceptionInfo: No such var: core/prime?
    data: {:file "example/spec.clj", :line 16}
java.lang.RuntimeException: No such var: core/prime?

В моей спецификации для factor я хочу использовать свой предикат prime? для проверки возвращаемых коэффициентов. Самое замечательное в этой спецификации factor то, что, если предположить, что prime? верна, она полностью документирует функцию factor и избавляет меня от необходимости писать какие-либо другие тесты для этой функции. Но если вы считаете, что это слишком круто, вы можете заменить его на pos? или что-то в этом роде.

Неудивительно, однако, что при повторной попытке boot run вы все равно получите сообщение об ошибке, на этот раз жалуясь, что спецификация :args либо для #'example.core/divisible?, либо для #'example.core/prime?, либо для #'example.core/factor (в зависимости от того, что произойдет раньше) отсутствует. Это связано с тем, что независимо от того, используете ли вы alias пространство имен или нет, fdef не используйте этот псевдоним, если указанный вами символ не называет переменную, которая уже существует. Если var не существует, символ не расширяется. (Для еще большего удовольствия удалите :as core из build.boot и посмотрите, что произойдет.)

Если вы хотите сохранить этот псевдоним, вам нужно удалить (:require [example.spec]) из example.core и добавить (require 'example.spec) к build.boot. Конечно, этот require должен идти после example.core, иначе он не сработает. И в этот момент, почему бы просто не вставить require прямо в example.spec?

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


person Sam Estep    schedule 25.06.2016    source источник
comment
Вы прекрасно доказываете, почему предпочтительнее иметь спецификацию в одном и том же пространстве имен при использовании деструктуризации. Кажется невозможным избежать компромисса в получении более точного интерфейса за счет загромождения кода, но было бы здорово, если бы он был... так что я надеюсь, что кто-то сможет ответить на этот вопрос :)   -  person Timothy Pratley    schedule 25.06.2016
comment
Я полагаю, что предполагаемая практика состоит в том, чтобы требовать example.spec в example.core и просто alias example.core в example.spec вместо того, чтобы требовать этого...   -  person Leon Grapenthin    schedule 26.06.2016
comment
@LeonGrapenthin Это не работает; см. мое последнее редактирование.   -  person Sam Estep    schedule 27.06.2016
comment
@SamEstep В случае prime? это сводится к обычной проблеме циклической зависимости: вы не можете использовать переменные ns A в ns B, если вы также используете ns B в ns A, независимо от того, делаете ли вы это для написания спецификаций. Я считаю, что проблема решения, о которой вы упомянули, была решена (но не решена) для этой фиксации: .com/clojure/clojure/commit/ (сейчас только на мастере) — Чтение из источника должно вызвать ошибку в вашем случае. Невозможно решить...   -  person Leon Grapenthin    schedule 27.06.2016
comment
@SamEstep Что вы можете попробовать, так это использовать полностью квалифицированный ns для ваших fdefs и вообще не требовать или использовать псевдоним example.core. В качестве альтернативы можно утверждать, что если ваш код зависит от синтаксического анализатора спецификации, спецификация становится артефактом код и, как таковой, должен идти непосредственно в код.   -  person Leon Grapenthin    schedule 27.06.2016
comment
@LeonGrapenthin Из того, что я прочитал в clojure.spec обосновании и guide, похоже, мой вариант использования вполне допустим и предназначен.   -  person Sam Estep    schedule 27.06.2016
comment
@SamEstep Я действительно не понимаю, почему вы и Леванд вообще хотите поместить спецификацию в отдельный ns. В своем ответе он не приводит никаких аргументов, почему это следует делать, и чем больше я смотрю на проблему, которую вы здесь подробно изложили, я полагаю, что без какой-либо новой функции его ответ не будет иметь места.   -  person Leon Grapenthin    schedule 27.06.2016
comment
@LeonGrapenthin Это именно то, что я хочу сказать. Насколько я могу судить, куда разумнее поместить спецификации и функции в одно и то же пространство имен. Я разместил этот вопрос, потому что увидел, что ответ Леванда получил так много голосов, попытался применить его на практике, увидел, что это не сработало, и хотел посмотреть, не упустил ли я что-то.   -  person Sam Estep    schedule 27.06.2016
comment
@SamEstep Я перечитал его ответ, и аргумент, который он приводит, - читабельность IHO. Я прокомментировал его ответ и проиллюстрировал другой рабочий процесс, который позволяет разделить файлы без разделения пространства имен.   -  person Leon Grapenthin    schedule 27.06.2016


Ответы (1)


Этот вопрос демонстрирует важное различие между спецификациями, используемыми внутри приложения, и спецификациями, используемыми для тестирования приложения.

Спецификации, используемые в приложении для согласования или проверки ввода, например :example.core/config здесь, являются частью кода приложения. Они могут находиться в том же файле, где используются, или в отдельном файле. В последнем случае код приложения должен :require соответствовать спецификациям, как и любой другой код.

Спецификации, используемые в качестве тестов, загружаются после определяемого ими кода. Это ваши fdef и генераторы. Вы можете поместить их в отдельное пространство имен от кода — даже в отдельный каталог, не упакованный с вашим приложением — и они будут :require кодом.

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

person Stuart Sierra    schedule 09.06.2017