Should we really use Clojure's syntax for namespaced keys?


#21

My knowledge of Spec is still a bit superficial, but I think this is what :req-un and :opt-un are for?

You can definitely check unqualified keywords using spec. But namespaced qualified keys when used in maps get a special treatement, as their spec is globally enforced once registered. Consider the following example:

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

(s/def :mc.cust/first-name string?)
(s/def :mc.contact/email string?)

(defn greet [customer]
  (str "Hello, " (:mc.cust/first-name customer)))

(s/fdef greet
        :args (s/cat :customer (s/keys :req [:mc.cust/first-name])))

(stest/instrument `greet)


(greet {:mc.cust/first-name "Bob" :mc.contact/email {}})
;; Throws, the following:
ExceptionInfo Call to #'user/greet did not conform to spec:
In: [0 :mc.contact/email] val: {} fails spec: :mc.contact/email at: [:args :customer :mc.contact/email] predicate: string?
clojure.core/ex-info (core.clj:4739)

Our spec’ed function only defined :mc.cust/first-name, and really it is the only thing it cares about. But because :mc.cust/email is a fully qualified keyword in the standard sense, spec enforces that if it is present in the map argument, it has to be a string.

I think it’s a really smart way of keeping dynamicity in the system. You do not have to provide for each function spec all the :opt-un [...] for every possible piece of information that might flow through your system. But you still maintain a strong integrity of the value attached to your global names, and hopefully you get an error message closer to where you corrupted your data.

As I said before, maybe it’s just a matter of convincing spec to treat “all-terrain” keywords as fully-qualified to regain that property.


#22

@chpill In your opinion, what do I lose if I transform your example to the following?

(require '[clojure.spec.alpha :as s])
(require '[clojure.spec.test.alpha :as stest])
(require '[my-company.specs :as msp])

(s/def ::msp/mc_cust_first_name string?)
(s/def ::msp/mc_cust_email string?)

(defn greet [customer]
  (str "Hello, " (:mc_cust_first_name customer)))

(s/fdef greet
        :args (s/cat :customer (s/keys :req-un [::msp/mc_cust_first_name])))

#23

@vvvvalvalval calling (greet {:mc_cust_first_name "Bob" :mc_cust_email 42}) won’t raise any error here. We would need to provide an :opt-un [::msp/mc_cust_email] to detect an issue early.

The example here isn’t very interesting because the function does not return a map, but we write and compose functions that assoc, dissoc, update on maps all the time. They only deal with a subset of possible keys , and do not assume much about the rest. That bring us great composability, and also a great clarity thanks to the -> thread first macro.

Being able to continue using that style while enforcing globally that pieces of information are valid wherever they may be is what spec is about I think. you lose that by not using classical namespaced qualified keywords in the maps you pass around your program.


#24

Oh I see. Well, as I said above, I believe that this is a limitation of Spec, not of the program - that’s spec saying “I will encourage you to use Clojure’s convention for namespacing keys and I refuse to cooperate with a system that doesn’t use this convention.”.

This could be solved by allowing spec to accept non-Clojure-namespaced keys. Maybe with an s/def-un macro:

(s/def-un :mc_cust_first_name string?)

This way the fact that the key needs to be unique would still be very ‘in the face’ of the user.


#25

I believe that this is a limitation of Spec

There’s no such restrictions. The name of the spec is not the same as the key on an associative data structure. Spec must be keyed by a namespaced keyword, but your data being specced need not. You can use s/keys with req-un and opt-un for that.

So you’d have:

(s/def :customer/customer_name string?)
(s/def :customer (s/keys :req-un [:customer/customer_name]))

Which would spec the following associative:

(s/valid :customer/customer_name {:customer_name "John Doe"})
true

So as long as your other systems can support a colon as their first character, you can keep their name the same accross Clojure. You’ll still need to coerce the keyword into a string and back at the boundary though.

There’s actually also a way to do this with string keys or any other using s/keys* I was told, but I haven’t tried it. That way you could even keep the type a string in Clojure, meaning you wouldn’t even need to coerce the types back and forth.

My recommendation if you were really interested would be to rally around JSON and model your data inside Clojure to always be valid JSON, that seems like it’ll give you the biggest reach. If you need more powerful modeling then JSON affords, I would look into Transit or ION as the next level up, both will have a good reach and compatibility across languages.


#26

you can use s/keys with req-un and opt-un for that.

It seems to me though that @chpill just demonstrated that req-un and opt-un have limitations that req and opt don’t have?

This seems to be confirmed by Spec’s rationale: “Note that this cannot convey the same power to unqualified keywords as have namespaced keywords - the resulting maps are not self-describing.”

So as long as your other systems can support a colon as their first character, you can keep their name the same accross Clojure.

I really don’t think the colon matters :slight_smile: it’s fine in practice if the key is :customer_first_name in Clojure and customer_first_name in JS / GraphQL / Postgres / ElasticSearch / whatever, they’re equivalent for most practical purposes (including text-based search, which is really what I want to emphasize here).


#27

I’d follow the conventions of the language or the format I’m serializing to.

For JS/JSON I’d convert namespaced keywords to camelCase with no hyphens or dots, strip out com.my-comp if it’s there. Convert to the Clojure convention if I ever got it back.


#28

I’ve been there (I just went through a massive refactoring of our JS code to use namespaced keys, because keeping track of the data was harder and harder). That’s essentially Approach 1. I strongly recommend against it. Here’s what I took away from this experience:

  1. The benefits of having namespaced / globally-unique keys outweighs the convenience of following JS convention for keys (or of having short keys for that matter)
  2. What matters is the ability to identify a key at first sight without any more context, and to perform whole-system searches of the uses of a key.

#29

Just an off the wall thought here that might be obvious;
If your primary concern is search-ability… consider thinking of the separator as a dot instead of an underscore or hypen. :slight_smile:

$ ag foo.bar
foo.txt
1:foo_bar
2:foo-bar

baz.cljs
64: (let [m {:foo.bar/booz {“baz%?” 2}}]

Instead of searching for my.company/foo-bar or my_company_foo_bar, if you just search for
"my.company.foo.bar" you will find all references (assuming your search supports regex, which most do). Regex matching “.” doesn’t care if you use underscores, hypens, slashes.

So maybe it’s worth thinking of the separator as . even if it’s not /shrug

For camel case it’s a bit harder to just think of it as . because you need .? to match:
ag foo.?bar
foo.txt
1:fooBar
2:foo_bar
3:foo-bar

Possible, but not fun.

I realize it is somewhat tangential to the discussion… I’m just offering this as a practical approach to surviving where various styles exist. :slight_smile: