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

Один из примеров в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"]}]

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

(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.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 пространство имен spec из пространства имен реализации по нескольким причинам. Вот пример, который использует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?

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

Ответы на вопрос(1)

Ваш ответ на вопрос