Welcome! Please see the About page for a little more info on how this works.

0 votes
in Clojure by
edited by

I think there would be value in having Spec conform the literal code passed to functions the same way they do for macros. Maybe fdef could take a :syntax spec as well which would be used for that.

There are quite a few functions I think this could be useful for. A lot of functions will take options for example as keywords, those are most likely going to be provided as literals, so conforming their usage for function calls at read time would be valuable.

Other examples where this could be useful are say even for simple usages like +, where the args often contain literal numbers as well, where again the spec could validate that say a keyword or string literal isn't passed to it at read time as a syntax error.

In effect, I find a lot of Clojure functions themselves have function specific syntax, and it would be great if Spec could be used to conform their syntax at read time the same way it lets us do so for macros.

2 Answers

0 votes

Related to this, as you might know, clj-kondo does similar checks using static analysis.

More info on this: https://github.com/borkdude/clj-kondo/blob/master/doc/types.md

I also have an experimental project that extracts spec information and spits out clj-kondo type annotations: http://github.com/clj-kondo/inspector

Malli will do something similar, so any function using malli's defn with schema info can be linted accordingly with clj-kondo.

I think clj-kondo goes a step further in some way. I'm simply thinking of syntactic Spec conforming at read time. It wouldn't include any knowledge of other functions return types or such things.

I imagine something like:

(defn foo [a b & options]

(s/fdef foo
  :syntax (s/cat :a (s/or symbol? list? int?)
                  :b (s/or symbol? list? string?)
                  :options (s/cat :one (s/? (s/cat :opt #{:one} :val (s/or symbol? list? boolean?)))
                                  :two (s/? (s/cat :opt #{:two} :val (s/or symbol? list? #{:fast :slow}))))

(foo 10 (get-b) :one true :two :slow)

So like the `fdef` spec takes an additional `:syntax` spec that is just used to conform the syntax of calls to the function. That would be purely syntactical, which is why you see I often allow `(s/or symbol? list? ...)`, probably it be nice to provide a convenient spec here as well for something that is either a symbol or a form or a literal, since all function args will follow this pattern.

But still, this provides a lot of value, and seems quite simple to add, without needing to create any notion of types, and tracking of types across the program. Seems a good fit to Spec. It just allows the same kind of Spec syntax checking macros already get to functions as well. So here it prevents me from passing an unsupported option, from making a typo in the option keywords the function accepts, from passing a wrong enum value to the option, etc. So at least it covers all use of literals.
0 votes

Maybe we don't even need a separate :syntax Spec actually. Might be we can use the :args spec, but in a way where all arguments are implicitly wrapped in a (s/or symbol? list? arg-spec). That would probably be most convenient. So the :args spec would this way be able to be applied at read time to validate function syntax (thus use of literals and argument count/order) just as it is for macros.