25

Одним из примеров в 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?

Все эти проблемы будут решены путем размещения спецификаций в том же файле, что и реализации. Итак, должен ли я действительно помещать спецификации в отдельные пространства имен из реализаций? Если да, то как могут быть решены проблемы, описанные выше?

+3

Вы делаете отличный случай для того, почему предпочтительнее иметь спецификацию в том же пространстве имен при использовании деструктурирования. Кажется невозможным избежать компромисса с получением более точного интерфейса за счет загромождения кода, но было бы здорово, если бы было ... так что я надеюсь, что кто-то может ответить на это :) –

+0

Я считаю, что предполагаемая практика - потребовать 'example.spec' в' example.core' и просто 'alias'' example.core' в 'example.spec' вместо того, чтобы его требовать ... –

+0

@LeonGrapenthin Это не работает; см. мое последнее редактирование. –

ответ

6

Этот вопрос демонстрирует важное различие между спецификациями используемых в приложение и спецификацию используется для теста приложения.

Характеристики, используемые в приложении для соответствия или проверки ввода - например, :example.core/config здесь - являются частью кода приложения. Они могут быть в том же файле, где они используются, или в отдельном файле. В последнем случае код приложения должен :require спецификаций, как и любой другой код.

Спецификации, используемые в качестве тестов, загружаются после указанного им кода. Это ваши fdef s и генераторы.Вы можете поместить их в отдельное пространство имен из кода - даже в отдельный каталог, не упакованный с вашим приложением, - и они будут :require кода.

Возможно, у вас есть некоторые предикаты или функции полезности, которые используются обоими типами спецификаций. Они будут размещаться в отдельном пространстве имен.

 Смежные вопросы

  • Нет связанных вопросов^_^