Подтвердить что ты не робот

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

Один из примеров в clojure.spec Guide является простой опцией разбора:

(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"]}]

Позже в разделе validation определена функция, которая внутренне 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?)

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

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

Однако у меня возникли проблемы с тем, как это работает с destructuring. В качестве примера я собрал небольшой проект 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?

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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