Lets say we have a macro which takes one required argument followed by optional positional arguments like
(require '[clojure.spec :as spec]
'[clojure.spec.gen :as gen])
(defmacro dress [what & clothes]
`(clojure.string/join " " '(~what ~#clothes)))
(dress "me")
=> "me"
(dress "me" :hat "favourite")
=> "me :hat favourite"
and we write it a spec for it like
(spec/def ::hat string?)
(spec/fdef dress
:args (spec/cat :what string?
:clothes (spec/keys* :opt-un [::hat]))
:ret string?)
we'll find that spec/exercise-fn fails to exercise the macro
(spec/exercise-fn `dress)
;1. Unhandled clojure.lang.ArityException
; Wrong number of args (1) passed to: project/dress
even though the data generated by the functions generator is accepted just fine by the macro:
(def args (gen/generate (spec/gen (spec/cat :what string?
:clothes (spec/keys* :opt-un [::hat])))))
; args => ("mO792pj0x")
(eval `(dress ~#args))
=> "mO792pj0x"
(dress "mO792pj0x")
=> "mO792pj0x"
Defining a function and exercising it the same way works fine on the other hand:
(defn dress [what & clothes]
(clojure.string/join " " (conj clothes what)))
(spec/def ::hat string?)
(spec/fdef dress
:args (spec/cat :what string?
:clothes (spec/keys* :opt-un [::hat]))
:ret string?)
(dress "me")
=> "me"
(dress "me" :hat "favourite")
=> "me :hat favourite"
(spec/exercise-fn `dress)
=> ([("") ""] [("l" :hat "z") "l :hat z"] [("") ""] [("h") "h"] [("" :hat "") " :hat "] [("m") "m"] [("8ja" :hat "N5M754") "8ja :hat N5M754"] [("2vsH8" :hat "Z") "2vsH8 :hat Z"] [("" :hat "TL") " :hat TL"] [("q4gSi1") "q4gSi1"])
And if we take a look at the built in macros with similar definition patterns we'll see the very same issue:
(spec/exercise-fn `let)
; 1. Unhandled clojure.lang.ArityException
; Wrong number of args (1) passed to: core/let
One interesting thing is that exercise-fn works fine when there's always one required named argument present:
(defmacro dress [what & clothes]
`(clojure.string/join " " '(~what ~#clothes)))
(spec/def ::hat string?)
(spec/def ::tie string?)
(spec/fdef dress
:args (spec/cat :what string?
:clothes (spec/keys* :opt-un [::hat] :req-un [::tie]))
:ret string?)
(dress "me" :tie "blue" :hat "favourite")
=> "me :tie blue :hat favourite"
(spec/exercise-fn `dress)
In other words: There seems to be some hidden arguments always passed to macros during normal invocation which aren't passed by spec. Sadly I'm not experienced enough with Clojure to know about such details, but a little bird told me that there are things named &env and &form.
But my question boils down to: Is it possible to spec a macro with named arguments in such a way that spec/exercise-fn can give it a good workout?
Addendum:
Wrapping keys* with an and seems to break exercise-fn again, even if it has a required named arg.
You can't use exercise-fn with macros as you can't use apply with macros. (Note that it's called exercise fn :).
This is exactly like (apply dress ["foo"]), which yields the familiar "can't take value of a macro". The different error message you see is because it's applying to the var rather than the macro, as what's really happening is like (apply #'user/dress ["foo"]).
Related
My Clojure spec looks like :
(spec/def ::global-id string?)
(spec/def ::part-of string?)
(spec/def ::type string?)
(spec/def ::value string?)
(spec/def ::name string?)
(spec/def ::text string?)
(spec/def ::date (spec/nilable (spec/and string? #(re-matches #"^\d{4}-\d{2}-\d{2}$" %))))
(spec/def ::interaction-name string?)
(spec/def ::center (spec/coll-of string? :kind vector? :count 2))
(spec/def ::context- (spec/keys :req [::global-id ::type]
:opt [::part-of ::center]))
(spec/def ::contexts (spec/coll-of ::context-))
(spec/def ::datasource string?)
(spec/def ::datasource- (spec/nilable (spec/keys :req [::global-id ::name])))
(spec/def ::datasources (spec/coll-of ::datasource-))
(spec/def ::location string?)
(spec/def ::location-meaning- (spec/keys :req [::global-id ::location ::contexts ::type]))
(spec/def ::location-meanings (spec/coll-of ::location-meaning-))
(spec/def ::context string?)
(spec/def ::context-association-type string?)
(spec/def ::context-association-name string?)
(spec/def ::priority string?)
(spec/def ::has-context- (spec/keys :req [::context ::context-association-type ::context-association-name ::priority]))
(spec/def ::has-contexts (spec/coll-of ::has-context-))
(spec/def ::fact- (spec/keys :req [::global-id ::type ::name ::value]))
(spec/def ::facts (spec/coll-of ::fact-))
(spec/def ::attribute- (spec/keys :req [::name ::type ::value]))
(spec/def ::attributes (spec/coll-of ::attribute-))
(spec/def ::fulltext (spec/keys :req [::global-id ::text]))
(spec/def ::feature- (spec/keys :req [::global-id ::date ::location-meanings ::has-contexts ::facts ::attributes ::interaction-name]
:opt [::fulltext]))
(spec/def ::features (spec/coll-of ::feature-))
(spec/def ::attribute- (spec/keys :req [::name ::type ::value]))
(spec/def ::attributes (spec/coll-of ::attribute-))
(spec/def ::ioi-slice string?)
(spec/def ::ioi- (spec/keys :req [::global-id ::type ::datasource ::features ::attributes ::ioi-slice]))
(spec/def ::iois (spec/coll-of ::ioi-))
(spec/def ::data (spec/keys :req [::contexts ::datasources ::iois]))
(spec/def ::data- ::data)
But it fails to generate samples with:
(spec/fdef data->graph
:args (spec/cat :data ::xml-spec/data-))
(println (stest/check `data->graph))
then it will fail to generate with an exception:
Couldn't satisfy such-that predicate after 100 tries.
It is very convenient to generate spec automatically with stest/check but how to beside spec also have generators?
When you see the error Couldn't satisfy such-that predicate after 100 tries. when generating data from specs, a common cause is an s/and spec because spec builds generators for s/and specs based solely on the first inner spec.
This spec seemed most likely to cause this, because the first inner spec/predicate in the s/and is string?, and the following predicate is a regex:
(s/def ::date (s/nilable (s/and string? #(re-matches #"^\d{4}-\d{2}-\d{2}$" %))))
If you sample a string? generator, you'll see what it produces is unlikely to ever match your regex:
(gen/sample (s/gen string?))
=> ("" "" "X" "" "" "hT9" "7x97" "S" "9" "1Z")
test.check will try (100 times by default) to get a value that satisfies such-that conditions, then throw the exception you're seeing if it doesn't.
Generating Dates
You can implement a custom generator for this spec in several ways. Here's a test.check generator that will create ISO local date strings:
(def gen-local-date-str
(let [day-range (.range (ChronoField/EPOCH_DAY))
day-min (.getMinimum day-range)
day-max (.getMaximum day-range)]
(gen/fmap #(str (LocalDate/ofEpochDay %))
(gen/large-integer* {:min day-min :max day-max}))))
This approach gets the range of valid epoch days, uses that to control the range of large-integer* generator, then fmaps LocalDate/ofEpochDay over the generated integers.
(def gen-local-date-str
(gen/fmap #(-> (Instant/ofEpochMilli %)
(LocalDateTime/ofInstant ZoneOffset/UTC)
(.toLocalDate)
(str))
gen/large-integer))
This starts with the default large-integer generator and uses fmap to provide a function that creates a java.time.Instant from the generated integer, converts it to a java.time.LocalDate, and converts that to a string which happens to conveniently match your date string format. (This is slightly simpler on Java 9 and above with java.time.LocalDate/ofInstant.)
Another approach might use test.chuck's regex-based string generator, or different date classes/formatters. Note that both of my examples will generate years that are eons before/after -9999/+9999, which won't match your \d{4} year regex, but the generator should produce satisfactory values often enough that it may not matter for your use case. There are many ways to generate date values!
(gen/sample gen-local-date-str)
=>
("1969-12-31"
"1970-01-01"
"1970-01-01"
...)
Using Custom Generators with Specs
Then you can associate this generator with your spec using s/with-gen:
(s/def ::date
(s/nilable
(s/with-gen
(s/and string? #(re-matches #"^\d{4}-\d{2}-\d{2}$" %))
(constantly gen-local-date-str))))
(gen/sample (s/gen ::date))
=>
("1969-12-31"
nil ;; note that it also makes nils b/c it's wrapped in s/nilable
"1970-01-01"
...)
You can also provide "standalone" custom generators to certain spec functions that take an overrides map, if you don't want to tie the custom generator directly to the spec definition:
(gen/sample (s/gen ::data {::date (constantly gen-local-date-str)}))
Using this spec and generator I was able to generate your larger ::data spec, although the outputs were very large due to some of the collection specs. You can also control the size of those during generation using :gen-max options in the specs.
This is an application that represents visual patterns as a collection of Sshapes.
An Sshape (styled shape) is a list of points and a map of style information.
An APattern is a record containing a list of Sshapes.
Here's the spec :
In sshape.clj
(spec/def ::stroke-weight int?)
(spec/def ::color (spec/* int?))
(spec/def ::stroke ::color)
(spec/def ::fill ::color)
(spec/def ::hidden boolean?)
(spec/def ::bezier boolean?)
(spec/def ::style (spec/keys :opt-un [::stroke-weight ::stroke ::fill ::hidden ::bezier]))
(spec/def ::point (spec/* number?))
(spec/def ::points (spec/* ::point))
(spec/def ::SShape (spec/keys :req-un [::style ::points]))
In groups.clj
(spec/def ::sshapes (spec/* :patterning.sshapes/SShape))
(spec/def ::APattern (spec/keys :req-un [::sshapes]))
Then in another file, I try to test that a superimpose function that puts two APatterns together is accepting APatterns
(defn superimpose-layout "simplest layout, two patterns located on top of each other "
[pat1 pat2]
{:pre [(spec/valid? :patterning.groups/APattern pat1)]}
(->APattern (concat (:sshapes pat1) (:sshapes pat2))) )
Without the pre-condition this runs.
With the pre-condition, I get this infinite recursion and stack overflow.
Exception in thread "main" java.lang.StackOverflowError, compiling:(/tmp/form-init7774655152686087762.clj:1:73)
at clojure.lang.Compiler.load(Compiler.java:7526)
at clojure.lang.Compiler.loadFile(Compiler.java:7452)
at clojure.main$load_script.invokeStatic(main.clj:278)
at clojure.main$init_opt.invokeStatic(main.clj:280)
at clojure.main$init_opt.invoke(main.clj:280)
at clojure.main$initialize.invokeStatic(main.clj:311)
at clojure.main$null_opt.invokeStatic(main.clj:345)
at clojure.main$null_opt.invoke(main.clj:342)
at clojure.main$main.invokeStatic(main.clj:424)
at clojure.main$main.doInvoke(main.clj:387)
at clojure.lang.RestFn.applyTo(RestFn.java:137)
at clojure.lang.Var.applyTo(Var.java:702)
at clojure.main.main(main.java:37)
Caused by: java.lang.StackOverflowError
at clojure.spec.alpha$regex_QMARK_.invokeStatic(alpha.clj:81)
at clojure.spec.alpha$regex_QMARK_.invoke(alpha.clj:78)
at clojure.spec.alpha$maybe_spec.invokeStatic(alpha.clj:108)
at clojure.spec.alpha$maybe_spec.invoke(alpha.clj:103)
at clojure.spec.alpha$the_spec.invokeStatic(alpha.clj:117)
at clojure.spec.alpha$the_spec.invoke(alpha.clj:114)
at clojure.spec.alpha$dt.invokeStatic(alpha.clj:742)
at clojure.spec.alpha$dt.invoke(alpha.clj:738)
at clojure.spec.alpha$dt.invokeStatic(alpha.clj:739)
at clojure.spec.alpha$dt.invoke(alpha.clj:738)
at clojure.spec.alpha$deriv.invokeStatic(alpha.clj:1480)
at clojure.spec.alpha$deriv.invoke(alpha.clj:1474)
at clojure.spec.alpha$deriv.invokeStatic(alpha.clj:1491)
at clojure.spec.alpha$deriv.invoke(alpha.clj:1474)
at clojure.spec.alpha$deriv.invokeStatic(alpha.clj:1491)
at clojure.spec.alpha$deriv.invoke(alpha.clj:1474)
at clojure.spec.alpha$deriv.invokeStatic(alpha.clj:1492)
at clojure.spec.alpha$deriv.invoke(alpha.clj:1474)
at clojure.spec.alpha$deriv.invokeStatic(alpha.clj:1492)
at clojure.spec.alpha$deriv.invoke(alpha.clj:1474)
at clojure.spec.alpha$deriv.invokeStatic(alpha.clj:1492)
etc.
Update :
OK. I've narrowed this down a bit in the repl.
Let's say a vector of points is defined so that pts is
[[-0.3 -3.6739403974420595E-17] [1.3113417037298127E-8 -0.2999999999999997] [0.2999999999999989 2.6226834037856828E-8] [-3.934025103841547E-8 0.29999999999999744] [-0.3 -3.6739403974420595E-17]]
Then calling
(spec/valid? :patterning.sshapes/points pts)
gives me the stack overflow :
StackOverflowError clojure.spec.alpha/regex? (alpha.clj:81)
So it looks like it just because I'm trying to match a spec/* of a spec/* of numbers.
Is there some reason that nested vectors trigger this kind of infinite recursion?
You should probably use spec/coll-of instead of s/* for this purpose:
(s/def ::point (s/coll-of number?))
(s/def ::points (s/coll-of ::point))
(s/def ::SShape (s/keys :req-un [::style ::points]))
(s/exercise (s/coll-of ::SShape))
;; => ([[] []] [[{:style {:hidden false, :bezier false}, :points [[1.0 -3.0 0 0.75 -1.0 -1.0 0 -1.5 1.0 3.0 -1 0] [-2.0 -1 2.0 2.0 0 ...
There are a couple of bugs in Clojure spec in this area, I believe.
This one looks like an instance of https://dev.clojure.org/jira/browse/CLJ-2002. It is triggered on conform:
(s/conform (s/* (s/* number?)) [[]]) ; => StackOverflowError
Not sure how to write a Spec to check the destructured arguments to a function.
If I have this simple function:
(defn make-name [[first last]]
(str first " " last))
And I write this Spec for the argument:
(s/def ::vstring2 (s/coll-of string? :kind vector? :count 2 :distinct false))
It works correctly:
(s/conform ::vstring2 ["Rich" "Hickey"])
=> ["Rich" "Hickey"]
But when I try to apply it to the function like so:
(s/fdef make-name
:args ::vstring2
:ret string?)
It blows up:
(stest/check `make-name)
=>
({:spec #object[clojure.spec.alpha$fspec_impl$reify__2451
0x7dd4c5ac
"clojure.spec.alpha$fspec_impl$reify__2451#7dd4c5ac"],
:clojure.spec.test.check/ret {:result #error{:cause "Wrong number of args (2) passed to: roster/make-name",
(...)
How do I write the function Spec using ::vstring2? Or do I?
Thank you.
You just need to specify another spec for your function's arguments e.g. using s/cat:
(s/fdef make-name
:args (s/cat :arg1 ::vstring2)
:ret string?)
In your example, the :args spec is expecting two arguments because your ::vstring2 spec is a collection expecting two strings. With this change, the function :args spec knows that it only takes one argument which should conform to ::vstring2.
There are more function spec examples here and here.
Say that we have a function clothe which requires one positional argument person in addition to a number of optional named arguments :hat, :shirt and :pants.
(defn clothe [person & {:keys [hat shirt pants]}]
(str "Clothing " person " with " hat shirt pants "."))
(clothe 'me :hat "top hat")
=> "Clothing me with top hat."
My current way of writing a spec for this function would be:
(require '[clojure.spec :as spec]
'[clojure.spec.gen :as gen])
(spec/def ::person symbol?)
(spec/def ::clothing
(spec/alt :hat (spec/cat :key #{:hat} :value string?)
:shirt (spec/cat :key #{:shirt} :value string?)
:pants (spec/cat :key #{:pants} :value string?)))
(spec/fdef clothe
:args (spec/cat :person ::person
:clothes (spec/* ::clothing))
:ret string?)
The problem then being that it allows for argument lists like
(clothe 'me :hat "top hat" :hat "nice hat")
=> "Clothing me with nice hat."
which even though allowed by the language itself probably is a mistake whenever made. But perhaps worse than that is that it makes the generated data unrealistic to how the function is usually called:
(gen/generate (spec/gen (spec/cat :person ::person
:clothes (spec/* ::clothing))))
=> (_+_6+h/!-6Gg9!43*e :hat "m6vQmoR72CXc6R3GP2hcdB5a0"
:hat "05G5884aBLc80s4AF5X9V84u4RW" :pants "3Q" :pants "a0v329r25f3k5oJ4UZJJQa5"
:hat "C5h2HW34LG732ifPQDieH" :pants "4aeBas8uWx1eQWYpLRezBIR" :hat "C229mzw"
:shirt "Hgw3EgUZKF7c7ya6q2fqW249GsB" :pants "byG23H2XyMTx0P7v5Ve9qBs"
:shirt "5wPMjn1F2X84lU7X3CtfalPknQ5" :pants "0M5TBgHQ4lR489J55atm11F3"
:shirt "FKn5vMjoIayO" :shirt "2N9xKcIbh66" :hat "K8xSFeydF" :hat "sQY4iUPF0Ef58198270DOf"
:hat "gHGEqi58A4pH2s74t0" :pants "" :hat "D6RKWJJoFLCAaHId8AF4" :pants "exab2w5o88b"
:hat "S7Ti2Cb1f7se7o86I1uE" :shirt "9g3K6q1" :hat "slKjK67608Y9w1sqV1Kxm"
:hat "cFbVMaq8bfP22P8cD678s" :hat "f57" :hat "2W83oa0WVWM10y1U49265k2bJx"
:hat "O6" :shirt "7BUJ824efBb81RL99zBrvH2HjziIT")
And worse of all, if you happen to have a recursive defenition with spec/* there is no way of limiting the number of potentially recursive occurences generated when running tests on the code.
So then my question becomes: Is there a way to specify named arguments to a function limiting the number of occurences per key to one?
If we look at the way the require macro is specced in clojure.core.specs we can see that it uses (spec/keys* :opt-un []) to specify the named arguments in the dependency list, such as :refer and :as in (ns (:require [a.b :as b :refer :all])).
(s/def ::or (s/map-of simple-symbol? any?))
(s/def ::as ::local-name)
(s/def ::prefix-list
(s/spec
(s/cat :prefix simple-symbol?
:suffix (s/* (s/alt :lib simple-symbol? :prefix-list ::prefix-list))
:refer (s/keys* :opt-un [::as ::refer]))))
(s/def ::ns-require
(s/spec (s/cat :clause #{:require}
:libs (s/* (s/alt :lib simple-symbol?
:prefix-list ::prefix-list
:flag #{:reload :reload-all :verbose})))))
The documentation doesn't mention what :req-un and :opt-un are for, but the Spec Guide on the other hand mentions that they are for specifying unqualified keys. Returning to our function defenition we could write it as:
(spec/def ::clothing (spec/keys* :opt-un [::hat ::shirt ::pants]))
(spec/def ::hat string?)
(spec/def ::shirt string?)
(spec/def ::pants string?)
(spec/fdef clothe
:args (spec/cat :person ::person
:clothes ::clothing)
:ret string?)
Sadly this doesn't help with the function accepting multiple instances of the same named argument
(stest/instrument `clothe)
(clothe 'me :hat "top hat" :hat "nice hat")
=> "Clothing me with nice hat."
Though it does mean that the generator maximally produces one instance of the same key which does help with the recursive specs.
(gen/generate (spec/gen (spec/cat :person ::person
:clothes ::clothing)))
=> (u_K_P6!!?4Ok!_I.-.d!2_.T-0.!+H+/At.7R8z*6?QB+921A
:shirt "B4W86P637c6KAK1rv04O4FRn6S" :pants "3gdkiY" :hat "20o77")
I used the last days to dig deeper into clojure.spec in Clojure and ClojureScript.
Until now I find it most useful, to use specs as guards in :pre and :post in public functions that rely on data in a certain format.
(defn person-name [person]
{:pre [(s/valid? ::person person)]
:post [(s/valid? string? %)]}
(str (::first-name person) " " (::last-name person)))
The issue with that approach is, that I get a java.lang.AssertionError: Assert failed: (s/valid? ::person person) without any information about what exactly did not met the specification.
Has anyone an idea how to get a better error message in :pre or :post guards?
I know about conform and explain*, but that does not help in those :pre or :post guards.
In newer alphas, there is now s/assert which can be used to assert that an input or return value matches a spec. If valid, the original value is returned. If invalid, an assertion error is thrown with the explain result. Assertions can be turned on or off and can even optionally be omitted from the compiled code entirely to have 0 production impact.
(s/def ::first-name string?)
(s/def ::last-name string?)
(s/def ::person (s/keys :req [::first-name ::last-name]))
(defn person-name [person]
(s/assert ::person person)
(s/assert string? (str (::first-name person) " " (::last-name person))))
(s/check-asserts true)
(person-name 10)
=> CompilerException clojure.lang.ExceptionInfo: Spec assertion failed
val: 10 fails predicate: map?
:clojure.spec/failure :assertion-failed
#:clojure.spec{:problems [{:path [], :pred map?, :val 10, :via [], :in []}], :failure :assertion-failed}
I think the idea is that you use spec/instrument to validate function input and output rather than pre and post conditions.
There's a good example toward the bottom of this blog post: http://gigasquidsoftware.com/blog/2016/05/29/one-fish-spec-fish/ . Quick summary: you can define a spec for a function, including both input and return values using the :args and :ret keys (thus replacing both pre and post conditions), with spec/fdef, instrument it, and you get output similar to using explain when it fails to meet spec.
Minimal example derived from that link:
(spec/fdef your-func
:args even?
:ret string?)
(spec/instrument #'your-func)
And that's equivalent to putting a precondition that the function has an integer argument and a postcondition that it returns a string. Except you get much more useful errors, just like you're looking for.
More details in the official guide: https://clojure.org/guides/spec ---see under the heading "Spec'ing functions".
Without taking into account if you should use pre and post conditions to validate function arguments, there is a way to print somewhat clearer messages from pre and post conditions by wrapping your predicate with clojure.test/is, as suggested in the answer below:
How can I get Clojure :pre & :post to report their failing value?
So then your code could look like this:
(ns pre-post-messages.core
(:require [clojure.spec :as s]
[clojure.test :as t]))
(defn person-name [person]
{:pre [(t/is (s/valid? ::person person))]
:post [(t/is (s/valid? string? %))]}
(str (::first-name person) " " (::last-name person)))
(def try-1
{:first-name "Anna Vissi"})
(def try-2
{::first-name "Anna"
::last-name "Vissi"
::email "Anna#Vissi.com"})
(s/def ::person (s/keys :req [::first-name ::last-name ::email]))
Evaluating
pre-post-messages.core> (person-name try-2)
would produce
"Anna Vissi"
and evaluating
pre-post-messages.core> (person-name try-1)
would produce
FAIL in () (core.clj:6)
expected: (s/valid? :pre-post-messages.core/person person)
actual: (not (s/valid? :pre-post-messages.core/person {:first-name "Anna Vissi"}))
AssertionError Assert failed: (t/is (s/valid? :pre-post-messages.core/person person)) pre-post-messages.core/person-name (core.clj:5)
This is useful when you don't want to use s/assert, or can not enable s/check-assserts. Improving on MicSokoli's answer:
:pre simply cares that the values returned are all truthy, so we can convert the return value "Success!\n" to true (for strictness) and throw an error with the explanation and the input data in case the output is not successful.
(defn validate [spec input]
(let [explanation (s/explain-str spec input)]
(if (= explanation "Success!\n")
true
(throw (ex-info explanation {:input input}))))
A variation of this could be this one, but it would run the spec twice:
(defn validate [spec input]
(if (s/valid? spec input)
true
(throw (ex-info (s/explain spec input) {:input input}))))
Usage:
(defn person-name [person]
{:pre [(validate ::person person)]}
(str (::first-name person) " " (::last-name person)))