25

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?

+3

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 :) –

+0

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 ... –

+0

@LeonGrapenthin Che non funziona; guarda la mia ultima modifica. –

risposta

6

Questa domanda dimostra un'importante distinzione tra le specifiche utilizzate all'interno un'applicazione e le specifiche utilizzate per prova l'applicazione.

Le specifiche utilizzate nell'applicazione per conformarsi o convalidare l'input - come :example.core/config qui - fanno parte del codice dell'applicazione. Possono trovarsi nello stesso file in cui sono utilizzati o in un file separato. In quest'ultimo caso, il codice dell'applicazione deve essere :require, come qualsiasi altro codice.

Le specifiche utilizzate come test vengono caricate dopo il il codice specificato. Questi sono i tuoi fdef s e generatori.È possibile inserirli in uno spazio dei nomi separato dal codice, anche in una directory separata, non compresso con l'applicazione, e il codice sarà :require.

È possibile che si disponga di alcuni predicati o funzioni di utilità che vengono utilizzati da entrambi i tipi di specifiche. Questi andrebbero in uno spazio dei nomi separato tutto loro.