Fork me on GitHub
Andreas Liljeqvist12:04:09

How would I spec a map like {:type akeyword :args dependsonkey}?

Alex Miller (Clojure team)12:04:03

s/multi-spec is designed to handle cases where you choose the spec based on the data (here the :type)

Alex Miller (Clojure team)12:04:18

So that may be a good match here

Andreas Liljeqvist12:04:50

I can use a multimethod to spec the map depending on type (multimethod methodname :akeyword [m] (s/keys [:args]))

Alex Miller (Clojure team)12:04:27

Yes, that’s how s/multi-spec works

Andreas Liljeqvist12:04:45

But I can't see at the moment how I would specify that :args should have a different spec for that match

Andreas Liljeqvist12:04:55

Like that it should accept string? if :type is :a, but int? if :type is :b

Alex Miller (Clojure team)13:04:36

Is :args actually unnamespaced?

Andreas Liljeqvist13:04:57

nah, everything should be namespaced

Alex Miller (Clojure team)13:04:37

When unsure how to spec something, it’s best to always return to how to represent the truth of your actual data. In this case you’re saying that you have one attribute that can have a variety of different structures

Alex Miller (Clojure team)13:04:07

So you need to capture that in the spec

Alex Miller (Clojure team)13:04:54

The spec for that attribute is A or B or C

Alex Miller (Clojure team)13:04:24

You then have a separate constraint that says that a particular value of :type should co-occur with a particular form of :args and you should capture that constraint as a separate predicate

Alex Miller (Clojure team)13:04:37

So you would model the attribute with s/or, the map with s/keys, and the constraint by s/and-ing the map and the constraint

Andreas Liljeqvist13:04:27

Thank you. s/and-ing is always powerful, only problem is that we have to provide a custom-gen.

Andreas Liljeqvist13:04:47

But usually I have to write custom-gens anyway...


What would you do for speccing [& args] ?

Alex Miller (Clojure team)14:04:04

(s/cat :args (s/* any?))


ah perfect thanks

Alex Miller (Clojure team)14:04:17

unless you have other knowledge about args

👍 4

Is it possible to have a multi-spec dispatch on the first value of a vector and return a Spec for the rest of the vector? i.e. take [:my-vec 1 "2"]. The multi-spec would dispatch on :my-vec and each defmethod would return a spec for (vec (rest [:my-vec 1 "2"])) - [1 "2"].

Alex Miller (Clojure team)17:04:26

no, but you could dispatch on the first value of a vector and return a spec for the whole vector


Yeah... It's just the Spec for the first part of the vector is uninteresting -- it's always going to be any?. This makes the return value for the defmethods very repetitive.


(defmethod event-vec :my-vec
  (s/cat :x any? :a int? :b string?)
That part will always be the same.

Alex Miller (Clojure team)17:04:15

if only there was a way to remove boilerplate syntax….


That's also possible. The problem there is that once I move that to a macro, I need to move all functions that register a method for that spec to be macros.


Would x always be any? You could have x be say #{:my-vec} so the spec is more specific to the event spec you are returning?


More specific for tests around that event


Yes, always any?.


but for that specific defmethod isnt’ x :my-vec?


Yes, but that's already guaranteed because the multimethod is called.


The API consists of a lot of functions that look like this:

(defn reg-my-thing 
  [id spec other-stuff]
  (defmethod my-multimethod id
    (s/cat :x any? :rest spec))
  ;; do other stuff
In order to do what you're saying I'd need make most of the API macros. That isn't the end of the world but it does make the code base a lot messier to do what seems like such a simple operation.

Alex Miller (Clojure team)17:04:30

@kenny re “The problem there is that once I move that to a macro, I need to move all functions that register a method for that spec to be macros. ” - why?


Because the above code will not work.


cat needs a form, not a symbol.


(defmacro reg-my-thing
  [id spec other-stuff]
  `(defmethod my-multimethod id
    (s/cat :x any? :rest ~spec))
  ;; do other stuff

Alex Miller (Clojure team)17:04:17

I don’t think that macro is correct, but it can be fixed


It's not - more of psuedo code.


Essentially in order to program with spec, everything needs to be a macro.

Alex Miller (Clojure team)18:04:33

this is a macro already, it’s just not the right macro


Not sure I understand what you mean.


My API is defined a bunch of functions that are passed a spec for the (vec (rest [:my-vec 1 "2"])). The functions all do global registration sort of thing (akin to defmethod). Each of these functions needs to register a spec for the whole vector [:my-vec 1 "2"]. I could construct that spec at the macro level based on the spec they passed in, but that'd mean my whole API needs to be defined at the macro level.


... because this doesn't work 🙂

(defn reg-my-thing 
  [id spec other-stuff]
  (defmethod my-multimethod id
    (s/cat :x any? :rest spec))
  ;; do other stuff

Alex Miller (Clojure team)18:04:04

something like this works:

(require '[clojure.spec.alpha :as s])

(defmulti v first)
(defmethod v :hi [_]
  (s/cat :o #{:hi} :p #{:there}))

(s/def ::v (s/multi-spec v (fn [val tag] val)))

(s/valid? ::v [:hi :there])

(defmacro defvspec
  [op tail-spec]
  `(defmethod v ~op [_#] (s/cat :op #{~op} :rest ~tail-spec)))

(defvspec :a (s/cat :x int?))

(s/valid? ::v [:a 100])

Alex Miller (Clojure team)18:04:56

another option is to register your specs with s/def and then refer to them by their keyword name


Yes. Except that requires a breaking change to the API.

Alex Miller (Clojure team)18:04:08

which gets you out of caring about the form


reg-my-thing is passed a Spec in its arguments.

Alex Miller (Clojure team)18:04:47

if you have the spec instance, you can also have the macro invoke s/form to get back the form

Alex Miller (Clojure team)18:04:13

in that case I don’t know that you even need a macro


(defn reg-my-thing 
  [id spec other-stuff]
  (defmethod my-multimethod id
    (s/cat :x any? :rest (s/form spec)))
  ;; do other stuff


Won't that create a mess of the error messages?

Alex Miller (Clojure team)18:04:47

(defn defvspec2
  [op tail-spec]
  (defmethod v op [_] (eval `(s/cat :op any? :rest ~(s/form tail-spec)))))

Alex Miller (Clojure team)18:04:01

(s/valid? ::v [:b 10]) ;; true
(s/conform ::v [:b 10]) ;; {:op :b, :rest {:y 10}}
(s/explain ::v [:b nil]) 
;; In: [1] val: nil fails spec: :user/v at: [:b :rest :y] predicate: int?



Alex Miller (Clojure team)18:04:40

really the same thing you’re doing with a macro


This is a CLJS project so I'm not sure about the eval usage.

Alex Miller (Clojure team)18:04:21

oh sure, throw that in at the end :)


Should've mentioned that in the beginning 😬 It's essentially adding Spec to re-frame, thus the reg-* API.

Alex Miller (Clojure team)18:04:46

well then, I don’t know :)

Alex Miller (Clojure team)18:04:27

I don’t understand the constraints in cljs as well. There are some changes coming to spec that will help with stuff like this too but I’m not sure when or how they will play out in cljs.


I'm guessing everything will need to be done at the macro level. I think the only constraint is the lack of eval.


If I have a list of lists, is it possible to write a spec that enforces that no two sublists start with the same value?


@mv Sure, if you can write a predicate that tests for that, you can use that predicate in a spec (or even as a spec).


So just a standard function?


And I’m assuming s/valid?? I’m new to spec


Presumably you already have a spec for "list of lists"?


Not yet, but there is a spec being applied for the individual elements with s/*


OK, so when you have your spec for list of lists, then you just s/and that spec with your predicate and that's your complete spec.


(s/def ::list-list-spec (s/and (s/coll-of (s/coll-of ::sublist-element-spec)) sublists-have-unique-prefix))


(or something like that)


Bear in mind you may not be able to generate data from that spec (you might, but generation may produce sublists with identical first elements quite often so the check on the generated data might fail).


If that's important, you'll need to write a custom generator.


But if you're just getting started with spec, you may not need that. Yet 🙂


Yea I don’t think I need that yet, this is more to enforce a bug won’t come back