28

clojure.specガイドの例の 1 つは、単純なオプション解析仕様です。

(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 を損ないます。

ただし、これが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?factorpos?

ただし、当然のことながら、再試行してもエラーが発生しますboot run。今回は、 orまたは(最初に試行した方)の:args仕様が不足していると不平を言います。これは、名前空間であるかどうかに関係なく、指定したシンボルが既に存在するvar を指定しない限り、そのエイリアスを使用しないためです。var が存在しない場合、シンボルは展開されません。(さらに楽しむには、fromを削除して何が起こるか見てみましょう。)#'example.core/divisible?#'example.core/prime?#'example.core/factoraliasfdef:as corebuild.boot

そのエイリアスを保持したい場合は、(:require [example.spec])fromを削除してtoexample.coreを追加する必要があります。もちろん、それはfor のに来る必要があります。そうしないとうまくいきません。そして、その時点で、 を に直接入れてみませんか?(require 'example.spec)build.bootrequireexample.corerequireexample.spec

これらの問題はすべて、仕様を実装と同じファイルに入れることで解決できます。では、仕様を実装とは別の名前空間に置くべきでしょうか? もしそうなら、私が上で詳述した問題はどのように解決できますか?

4

1 に答える 1