Spec, Re-frame, and Choosing Keywords

Hi there – I’m quite new and am adding spec to my re-frame project to verify app state on updates. I’m also trying to structure the DB map. But it seems what I want to do isn’t allowed, so maybe my approach is off :slight_smile: Any pointers would be greatly appreciated.

In structuring the DB, one way to go is nested maps:

{:person {:name "Bob"
          :passion "Painting"}}

Another way for this example might be namespaced keywords:

{:person/name "Bob"
 :person/passion "Painting"}

I started with this, but when adding Spec I tried to do something like:

(s/def ::person/name string?)

But it didn’t seem to take – maybe because it wouldn’t be possible to resolve whether this is :person/name in the current namespace or :name in another namespace?

Is this not an idiomatic way of doing things? Am I using namespaced keywords incorrectly? Should I just stick with nested maps?

Thanks!

It should be :person/name with a single :. If you requre'd another namespaces, like

;;(require '[my.ns.person]) ;;EDIT: typo, pointed out by Sean below
(require '[my.ns.person :as person])

Then you can use ::person/name which will expand to :my.ns.person/name
See the section on keywords here:
https://clojure.org/reference/reader#_literals

As for being idiomatic, I think there is a trend towards increasing use of qualified (ie namespaced) keywords, but there are still situations where you may want or be fine with unqualified keywords. A major deciding factor is if the keys will end up inside maps where there may be name clashes. If so you can avoid this with qualified keys.

3 Likes

I assume you mean:

(require '[my.ns.person :as person])

otherwise you won’t get the person alias that you need for ::person/name to work.

2 Likes

Exactly, I forgot the actual alias

Thanks very much for your answers! I’ve tested this and it’s working.

On a related note – suppose I want to do something like:

(s/def :person/name string?)
(s/def :person/passion string?)
(s/def ::person 
  (s/keys :req [:person/name :person/passion]))

The :person map needs to be qualified, but it’s qualified under the current namespace whereas the :person/name and :person/passion keywords are “qualified” under a non-existant “:person” namespace. Correct?

It seems bad to have them be qualified to different namespaces, but I like being able to use :person/name.

You can use any qualified keyword you want for the Spec. :person/person for example. As you noted, the qualified keywords inside the hash map do not have to be tied to a code namespace – and they are “just” Specs – and neither does the overall Spec for the map.

2 Likes

The only argument I see for having :my.person/name be defined in the ns my.person is to make it simple to find the ns from the keyword.

Specs relying on a registry and namespace qualified keywords is a mechanism to give you the freedom to put your specs somewhere else.

1 Like

Specs relying on a registry and namespace qualified keywords is a mechanism to give you the freedom to put your specs somewhere else.

This makes sense to me. Supposing that I had two maps that shared a keyword, e.g. :name, and I wanted to spec this keyword differently:

(s/def :my.person/name string?)
(s/def :elonmusk.child/name int?)

Would you then separate those specs into different namespaces (and files)? To me, it seems like a lot to create a new file with a small number of specs, simply to namespace them – which is why the “un-namespaced” form :person/name worked for me.

Is there convention on where/how to organize specs in a project?

1 Like

I’d stick the specs in the same Clojure file unless you have a reason to do otherwise. Then you can also use the ::name shorthand to get a ns qualified key for the current ns.

An example for when you need to put specs somewhere else is if you need to add specs for someone else’s library.

I think the spec guide is the best resource you’ll find. If that doesn’t suffice, you can try posting in the Clojurians Slack and hope for a reply by Alex Miller :slight_smile:

https://clojure.org/guides/spec

Overall, I think I see where you’re coming from. Ns qualified keys didn’t make sense to me at first. I don’t think you have to worry too much about it. Ns qualified keys are great for avoiding conflicts. But if you don’t need that yet, normal keywords are probably fine!

2 Likes

No, but there are some considerations that might apply which would provide some guidance:

  • If you are writing a library that you want folks to be able to use on Clojure 1.8 or earlier, you must put the specs in a separate namespace that those users can simply ignore.
  • If you are writing a library targeting Clojure 1.9 or later, if you want usage of your specs to be optional for users, you still need to put them in a separate namespace that users can either require or not.
  • Otherwise, it’s generally easier to keep your function specs (fdef) with the functions that they describe (above them is probably clearer).
  • For data specs, it’s reasonable to keep them all together in one namespace, perhaps with predicate functions that they depend on.
  • If you have a large number of data specs, they’ll probably fall naturally into several different domain groups and each could be in its own namespace.

Specs can serve a lot of different purposes: I talk about the ways we use Spec at World Singles Networks in this blog post: https://corfield.org/blog/2019/09/13/using-spec/

3 Likes

Or hope for @seancorfield to reply here :smiley:

4 Likes

This is super helpful. Thanks very much for your response!