clojure namespaces destructuring cyclic-dependency clojure.spec

clojure - ¿Cómo puedo usar mis especificaciones para los fines previstos si están en un espacio de nombres diferente?



namespaces destructuring (1)

Uno de los ejemplos en la guía clojure.spec es una simple especificación de análisis de opciones:

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

Más adelante, en la sección de validation , se define una función que cumple internamente su entrada utilizando esta especificación:

(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

Dado que la guía debe ser fácil de seguir desde el REPL, todo este código se evalúa en el mismo espacio de nombres. En esta respuesta , sin embargo, @levand recomienda poner las especificaciones en espacios de nombres separados:

Normalmente pongo especificaciones en su propio espacio de nombres, junto con el espacio de nombres que están describiendo.

Esto rompería el uso de ::config arriba, pero ese problema puede remediarse:

Sin embargo, es preferible que los nombres de clave de especificación estén en el espacio de nombres del código, no en el espacio de nombres de la especificación. Esto todavía es fácil de hacer usando un alias de espacio de nombres en la palabra clave:

(ns my.app.foo.specs (:require [my.app.foo :as f])) (s/def ::f/name string?)

Continúa explicando que las especificaciones y las implementaciones se podrían colocar en el mismo espacio de nombres, pero no sería lo ideal:

Si bien ciertamente podría colocarlos junto con el código especificado en el mismo archivo, eso perjudica la legibilidad de la OMI.

Sin embargo, estoy teniendo problemas para ver cómo esto puede funcionar con la destructuring . Como ejemplo, armé un pequeño proyecto de Boot con el código anterior traducido a múltiples espacios de nombres.

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"])))

Pero claro, cuando ejecuto esto, me sale un error:

$ boot run clojure.lang.ExceptionInfo: Unable to resolve spec: :example.core/config

Podría solucionar este problema agregando (require ''example.spec) a build.boot , pero eso es feo y propenso a errores, y solo lo será más a medida que aumente mi número de espacios de nombres de especificaciones. No puedo require el espacio de nombres de especificación del espacio de nombres de implementación, por varias razones. Aquí hay un ejemplo que usa 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))))

El primer problema es el más obvio:

$ 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?

En mi especificación para el factor , quiero usar mi prime? predicado para validar los factores devueltos. Lo bueno de esta especificación de factor es que, ¿asumiendo que es prime? es correcto, documenta completamente la función de factor y elimina la necesidad de que yo escriba cualquier otra prueba para esa función. ¿Pero si crees que eso es demasiado bueno, puedes reemplazarlo con pos? o algo.

Sin embargo, como era de esperar, todavía obtendrá un error cuando intente boot run nuevo, esta vez quejándose de que la especificación :args para cualquiera de #''example.core/divisible? o #''example.core/prime? o #''example.core/factor (lo que ocurra al intentarlo primero) falta. Esto se debe a que, independientemente de si usted es un alias un espacio de nombres o no, fdef no usará ese alias a menos que el símbolo que le fdef tenga el nombre de una var que ya existe . Si la var no existe, el símbolo no se expande. (Para más diversión, elimine el :as core de build.boot y vea qué sucede).

Si desea mantener ese alias, debe eliminar (:require [example.spec]) de example.core y agregar un (require ''example.spec) a build.boot . Por supuesto, eso require que venga después del de example.core , o no funcionará. Y en ese punto, ¿por qué no poner simplemente el require directamente en example.spec ?

Todos estos problemas se resolverían al colocar las especificaciones en el mismo archivo que las implementaciones. Entonces, ¿realmente debería poner las especificaciones en espacios de nombres separados de las implementaciones? Si es así, ¿cómo pueden resolverse los problemas que he detallado anteriormente?


Esta pregunta demuestra una distinción importante entre las especificaciones utilizadas en una aplicación y las utilizadas para probar la aplicación.

Las especificaciones utilizadas dentro de la aplicación para conformar o validar la entrada, como :example.core/config aquí, son parte del código de la aplicación. Pueden estar en el mismo archivo donde se usan o en un archivo separado. En este último caso, el código de la aplicación debe :require las especificaciones, al igual que cualquier otro código.

Las especificaciones utilizadas como pruebas se cargan después del código que especifican. Estos son tus fdef y generadores. Puede colocarlos en un espacio de nombres separado del código, incluso en un directorio separado, no empaquetado con su aplicación, y ellos :require el código.

Es posible que tenga algunos predicados o funciones de utilidad que son utilizados por ambos tipos de especificaciones. Estos irían en un espacio de nombres por separado.