First off, this is basically a follow-up to my previous question 
<https://groups.google.com/forum/#!topic/clojure/qbe3D4_6KnY>. I fear I 
didn't express my concerns clearly so it was very
hard to answer the underlying questions.

*The Spec*

Let's say we have a spec, describing the AST for a very simple (and 
useless) language consisting of variable declarations
and function calls. We're using namespaced keywords since later processing 
steps might attach information to the AST.

https://gist.github.com/xsc/a2baff46d43324b640adca5a5ec55198#file-ast_spec-clj

An example conforming to this spec could be:

(s/explain-data
  :lang/ast
  {:lang/declarations
   [{:lang/variable-name "var"}]
   :lang/calls
   [{:lang/call-name "call", :lang/call-parameters ["othervar"]}]})
;; => nil

Now, clearly "othervar" has not been declared, so we should raise the issue 
and tell whoever created the AST that there
is a problem. And more importantly, where.

*Identifying that there is a problem*

Adding a semantic invariant to a spec is possible using "s/and" and a 
predicate. For example, our simple requirements
can be expressed as:

https://gist.github.com/xsc/a2baff46d43324b640adca5a5ec55198#file-ast_spec_with_predicate-clj

Unfortunately, this will only tell us _that_ something is broken (and which 
predicate is responsible) but since the
predicate is attached to the root of the AST, this is also the problem 
location we are returned:

(s/explain-data
  :lang/ast-with-predicates
  {:lang/declarations
   [{:lang/variable-name "var"}]
   :lang/calls
   [{:lang/call-name "call", :lang/call-parameters ["othervar"]}]})
;; => {:clojure.spec/problems [{:path [], 
;;                              :pred variable-names-declared-before-use?,
;;                              :val {:lang/declarations ..., ...},
;;                              :via [:lang/ast-with-predicates],
;;                              :in []}]}

*Identifying where the problem lies*

If we had known that the only declared variable is "var" we could (and 
would) have incorporated it into the spec:

(def variable-names-declared-before-use?
  #{"var"})

(s/def :lang/variable-name
  (s/and :lang/name variable-names-declared-before-use?))

However, our variable space is dynamic. Of course, we could just collect 
all available variable names and call
"(s/def :lang/variable-name ...)" to override the spec – but then our 
":lang/ast" spec is stateful and I can't imagine that
being seen as desirable. (Not to mention the resulting implications for 
multi-threaded environments.)

Another solution could be the introduction of a dynamic variable that is 
used by predicates. Then, before calling
"explain-data", the input is analysed and the dynamic variable bound:

https://gist.github.com/xsc/a2baff46d43324b640adca5a5ec55198#file-ast_spec_with_dynamic_var-clj

And this gives as the actual problematic location:

(verify-ast
  {:lang/declarations
   [{:lang/variable-name "var"}]
   :lang/calls
   [{:lang/call-name "call", :lang/call-parameters ["othervar"]}]})
;; => {:clojure.spec/problems [{:path [:lang/calls :lang/call-parameters],
;;                              :pred variable-name-declared-before-use?,
;;                              :val "othervar",
;;                              :via [:lang/ast :lang/calls :lang/call :lang
/call-parameters :lang/variable-name],
;;                              :in [:lang/calls 0 :lang/call-parameters 0
]}]}

This could be made a bit more generic by allowing any body to be run within 
the `binding` form, e.g. by introducing
a "with-ast-validation" macro. Still, not a very clean solution, in my 
opinion.

All in all, and here I'd very much like to be corrected,* it seems really 
hard to combine a static structural spec with a*
*semantic one while retaining validation granularity. *Especially, if 
namespaced keywords are in play.

(Without namespaced keywords, we could run two different AST specs on the 
same data. But I'm not sure the second spec
could be dynamically generated since there is still the need to register it 
globally, either causing a lot of one-off specs to
end up in the registry or having different threads overwrite each others'. 
In my previous question, Alex Miller mentioned
conforming of "s/form"-serialised data as a way of generating dynamic specs 
but I'm not sure whether that, apparently
unreleased, feature still requires you to touch the registry.)

*Summary*

I hope my use case is reasonably clear now: there are two steps, parsing 
and validation, whose in-between interface is
described using a structural AST spec.

Ideally, the validation step could just use a "refined" or "specialized" 
version of the AST spec, attaching _additional_
constraints to the original specs within a limited context. I feel that 
this is still completely in line with the desire of clojure.spec
to have global specs for namespaced keywords since any value matching the 
refined spec would perfectly match the
original one.

https://gist.github.com/xsc/a2baff46d43324b640adca5a5ec55198#file-ast_spec_with_specialisation-clj

So, is clojure.spec just not the right tool for this job? It just seems 
like a near-perfect fit and one could leverage so many
of its features here, like using "explain-data" to find, then format 
validation errors, or the fact that specs are bound to
key names to "globally" constrain certain keys instead of writing recursive 
predicates.

Should clojure.spec only be used for structural validation (with validation 
being done explicitly in a second pass) or if
validation errors with low granularity are okay?

-- 
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clojure@googlegroups.com
Note that posts from new members are moderated - please be patient with your 
first post.
To unsubscribe from this group, send email to
clojure+unsubscr...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
--- 
You received this message because you are subscribed to the Google Groups 
"Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to clojure+unsubscr...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to