Fork me on GitHub

Finally have a good example of an issue I keep running into with s/keys. Here is a polymorphic payload for a couple of websocket messages:

{:msg/id :foo/msg, :msg/data { ... foo/data ... }}
{:msg/id :bar/msg, :msg/data { ... bar/data ... }}
My basic intuition is to have a multi-spec that dispatches on :msg/id :
(defmulti msg-spec :msg/id)
But then I have a problem, because methods need to deal with a polymorphic :msg/data too:
(defmethod msg-spec :foo/msg [_] (s/keys :req [:msg/data !?]))
(defmethod msg-spec :bar/msg [_] (s/keys :req [:msg/data !?]))
All I can think of is to have the multi-spec on :msg/data instead. But that dispatch function will be much more complex and will need to infer, from the :msg/data alone, the value that was right there in :msg/id. Am I missing something?

Alex Miller (Clojure team)04:12:51

You can base your polymorphism on more than just a key - it's an arbitrary function

Alex Miller (Clojure team)04:12:50

So you could base it on both id and something in msg

Alex Miller (Clojure team)04:12:09

Or you could do more than one level of multispec


Right, I think I understand that and it seems to be the problem. The :msg/id is all I need to determine the type of :msg/data but it’s only available at that root level


With a multi-spec for :msg-data I’d need to look at what keys are in there, and in some cases what values, just to derive a tag was just there in the parent map


I've heard Rich's rebuttal of this kind of "contextual polymorphism" , but this example feels like something that will come up in practice, especially as people adopt qualified keywords in their apis. An ad-hoc binding of a map-key to a spec would be a bulletproof one-liner alternative to what would now have to be a complex, possibly buggy dispatch function.

Alex Miller (Clojure team)05:12:28

So are you saying that :mag/data has more than one spec? That seems wrong.


Yes indeed. The websocket library is embedding the msg/tag and the msg/data in its payload. It takes a [msg/tag msg/data] and we get this map.


So we have multiple specs for both the msg itself (omitted other keys that need specs) and the msg/data, both of which ultimately depend on the msg/tag.


Curious what’s your intuition on why this is wrong? This is something that comes up a lot in my experience whit what are now unqualified library apis, but it seems bound to happen more as people adopt namespaced kws?


In a nutshell, I can define that :msg/id is either a :foo/msg or a :bar/msg and that :msg/data is either a ::foo/spec or ::bar/spec but I can’t enforce that relationship at the msg level

Alex Miller (Clojure team)16:12:16

Qualified names should have meaningful stable semantics. Using qualified names should make this happen less if people are using sufficiently qualified names (which they should)

Alex Miller (Clojure team)16:12:15

Could you not wrap an s/and around specs on both of these to add a constraint?

Alex Miller (Clojure team)16:12:47

Are ::foo/spec and ::bar/spec just s/keys specs? If so, do they really need to be different or is just (s/keys) to validate all attrs sufficient?


Is the lack of docstrings on spec/def intentional?


I could of course just add the metadata to the var


alexmiller: yes we have different s/keys specs for the data itself

Alex Miller (Clojure team)17:12:30

lvh: it was not part of the initial impl. it’s a highly rated request in the jira system and Rich mentioned it as an idea to me long ago. it’s not obvious to me how to best implement it (where to put the meta) for all types of specs (when you consider things like (s/def ::a ::b) as kws don’t have meta). Do you have a var to add meta to?

Alex Miller (Clojure team)17:12:54

jfntn: it would help me to see a more detailed example


alexmiller, happy to put something more detailed together and ping you later today

Alex Miller (Clojure team)17:12:06

cool, I will be in and out today


alexmiller: Sometimes I do by accident, yes — but much to your point; there’s no real vars in spec most of the time; so presumably that’s not where tooling would want to go look


=> (s/conform
    (s/+ (s/cat :one (s/+ #{1 2 3})
                :alpha (s/? #{:a})))
    [1 2 :a 2])
[{:one [1 2], :alpha :a} {:one [2]}]
=> (s/conform
    (s/+ (s/cat :one (s/+ #{1 2 3})
                :alpha (s/? #{:a})))
    [1 2 :a 2 3])
[{:one [1 2], :alpha :a} [{:one [2 3]}]]


In the second example I am surprised with the extra vector around {:one [2 3]}


This doesn't seem consistent with a longer example:


=> (s/conform
    (s/+ (s/cat :one (s/+ #{1 2 3}) :alpha (s/? #{:a})))
    [1 2 :a 2 3 :a 2 3 :a 2])
[{:one [1 2], :alpha :a} [{:one [2 3], :alpha :a} {:one [2 3], :alpha :a} {:one [2]}]]