Один из примеров в 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
?
Все эти проблемы можно было бы решить, поместив спецификации в тот же файл, что и реализации. Итак, должен ли я действительно размещать спецификации в отдельных пространствах имен от реализаций? Если да, то как можно решить описанные выше проблемы?
example.spec
вexample.core
и простоalias
example.core
вexample.spec
вместо того, чтобы требовать этого... - person Leon Grapenthin   schedule 26.06.2016prime?
это сводится к обычной проблеме циклической зависимости: вы не можете использовать переменные ns A в ns B, если вы также используете ns B в ns A, независимо от того, делаете ли вы это для написания спецификаций. Я считаю, что проблема решения, о которой вы упомянули, была решена (но не решена) для этой фиксации: .com/clojure/clojure/commit/ (сейчас только на мастере) — Чтение из источника должно вызвать ошибку в вашем случае. Невозможно решить... - person Leon Grapenthin   schedule 27.06.2016clojure.spec
обосновании и guide, похоже, мой вариант использования вполне допустим и предназначен. - person Sam Estep   schedule 27.06.2016