Uno degli esempi nella clojure.spec
Guide è un semplice spec opzione-parsing:Come posso utilizzare le mie specifiche per gli scopi previsti se si trovano in uno spazio dei nomi separato?
(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"]}]
Successivamente, nella sezione validation, una funzione è definita che internamente conform
s suo ingresso utilizzando questa spec:
(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
Poiché la guida deve essere facile da seguire dal REPL, tutto questo codice viene valutato nello stesso spazio dei nomi. In this answer, però, @levand raccomanda mettendo specifiche in spazi dei nomi separati:
I usually put specs in their own namespace, alongside the namespace that they are describing.
Ciò rompere l'utilizzo di ::config
sopra, ma che problema può essere risolto:
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?)
Egli continua a spiegare che specifiche e implementazioni potrebbero essere messi nello stesso spazio dei nomi, ma non sarebbe l'ideale:
While I certainly could put them right alongside the spec'd code in the same file, that hurts readability IMO.
Tuttavia, ho difficoltà a vedere come questo può funzionare con destructuring. Ad esempio, ho creato un piccolo progetto Boot con il codice sopra tradotto in più domini.
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"])))
Ma naturalmente, quando ho eseguito questo, ottengo un errore:
$ boot run
clojure.lang.ExceptionInfo: Unable to resolve spec: :example.core/config
ho potuto risolvere questo problema aggiungendo (require 'example.spec)
a build.boot
, ma questo è brutto e soggetto a errori, e sarà solo diventare più così come il mio numero di spazi dei nomi delle specifiche aumenta. Non riesco a require
lo spazio dei nomi delle specifiche dallo spazio dei nomi dell'implementazione, per diversi motivi. Ecco un esempio che utilizza 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))))
Il primo problema è la più ovvia:
$ 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?
Nella mia spec per factor
, voglio usare il mio prime?
predicato per convalidare i fattori restituiti. La cosa interessante di questa specifica factor
è che, supponendo che prime?
sia corretto, entrambi documentano completamente la funzione factor
ed elimina la necessità per me di scrivere altri test per quella funzione. Ma se pensi che sia troppo bello, puoi sostituirlo con pos?
o qualcosa del genere.
sorprende, però, ci si può comunque ottenere un errore quando si tenta boot run
nuovo, questa volta lamentando che le specifiche :args
sia per #'example.core/divisible?
o #'example.core/prime?
o #'example.core/factor
(a seconda di quale capita di provare prima) non è presente. Questo perché, a prescindere dal fatto che sia o meno uno spazio dei nomi, fdef
non è utilizzare l' tale alias a meno che il simbolo che gli dai un nome var sia già esistente. Se la var non esiste, il simbolo non viene espanso. (Per divertirsi ancora di più, togliere il :as core
da build.boot
e vedere cosa succede.)
Se si desidera mantenere che alias, è necessario rimuovere la (:require [example.spec])
da example.core
e aggiungere un (require 'example.spec)
a build.boot
. Certamente, che require
deve venire dopo quello per example.core
o che non funzionerà. E a quel punto, perché non mettere semplicemente lo require
direttamente nello example.spec
?
Tutti questi problemi potrebbero essere risolti inserendo le specifiche nello stesso file delle implementazioni. Quindi, dovrei davvero mettere le specifiche in spazi dei nomi separati dalle implementazioni? In tal caso, come possono essere risolti i problemi sopra descritti?
Si fa un ottimo esempio del motivo per cui è preferibile avere le specifiche nello stesso spazio dei nomi quando si utilizza la destrutturazione. Sembra impossibile evitare il compromesso di ottenere un'interfaccia più precisa al costo di ingombrare il codice, ma sarebbe bello se ci fosse ... quindi spero che qualcuno possa rispondere a questo :) –
Credo che la pratica voluta sia quella di richiedere 'example.spec' in' example.core' e solo 'alias'' example.core' in 'example.spec' invece di richiederlo ... –
@LeonGrapenthin Che non funziona; guarda la mia ultima modifica. –