Одним из примеров в 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
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)))))
(configure ["-server" "foo" "-verbose" true "-user" "joe"])
;; set server foo
;; set verbose true
;; set user joe
;;=> nil
Поскольку руководство предназначено для простого выполнения из REPL, весь этот код оценивается в том же пространстве имен. В this answer, хотя, @levand рекомендует устанавливать спецификации в отдельных пространствах имен:
I usually put specs in their own namespace, alongside the namespace that they are describing.
Это нарушило бы использование ::config
выше, но эта проблема может быть решена:
It is preferable for spec key names to be in the namespace of the code, however, not the namespace of the spec. This is still easy to do by using a namespace alias on the keyword:
(ns my.app.foo.specs (:require [my.app.foo :as f])) (s/def ::f/name string?)
Он продолжает объяснять, что спецификации и реализации могли быть помещены в том же пространстве имен, но это не было бы идеально:
While I certainly could put them right alongside the spec'd code in the same file, that hurts readability 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?
В моей спецификации для factor
, я хочу использовать мой prime?
предикат для проверки возвращаемых факторов. Замечательная вещь об этом factor
заключается в том, что при условии, что prime?
верен, он полностью документирует функцию factor
и устраняет необходимость в том, чтобы я мог написать любые другие тесты для этой функции. Но если вы думаете, что это слишком круто, вы можете заменить его на pos?
или что-то в этом роде.
Unsurprisingly, однако, вы все равно получите ошибку при попытке 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
?
Все эти проблемы будут решены путем размещения спецификаций в том же файле, что и реализации. Итак, должен ли я действительно помещать спецификации в отдельные пространства имен из реализаций? Если да, то как могут быть решены проблемы, описанные выше?
Вы делаете отличный случай для того, почему предпочтительнее иметь спецификацию в том же пространстве имен при использовании деструктурирования. Кажется невозможным избежать компромисса с получением более точного интерфейса за счет загромождения кода, но было бы здорово, если бы было ... так что я надеюсь, что кто-то может ответить на это :) –
Я считаю, что предполагаемая практика - потребовать 'example.spec' в' example.core' и просто 'alias'' example.core' в 'example.spec' вместо того, чтобы его требовать ... –
@LeonGrapenthin Это не работает; см. мое последнее редактирование. –