Classifying the different approaches I've seen and some of their trade-offs.
Lately I have been struggling to come up with a good validation library in Clojure. And it led me to discover and evaluate the different approaches.
Spec is great at type specification, but is not so great for validation. Because spec intentionally avoids data transformations.
Especially on the web, most input values are strings. And frequently they need to be sanitized and transformed before they are ready to have logic performed on them. Since Spec does not allow you to transform data during validation, it is not sufficient for this use case.
Using spec for transformation is discouraged by the authors as mentioned in the link and alluded to in the documentation. However, Sean Corfield rightly pointed out in Clojure Slack that Spec is extensible to allow data transformations. And his company has a library which does so.
Here are some approaches I have seen.
This is a common theme for Clojure validation libraries. You first make a spec, and then the library will attempt to make the data you provide fit into that spec. So if the field is
int? and it finds a string, it will automatically convert using a known string-to-int conversion.
The big problem with this approach is the lack of control over the validation process. It is very common for me to have transformations and validations both before and after the user input is coerced.
For example when the user enters a monetary value, I trim and verify the field is not empty first (as a separate error) before bothering to convert to a number. After converting, I transform the floating point number entered by the user into a whole number of cents. Neither the blank-ness nor the cent transformation is representable by Spec and cannot be reverse engineered from an
Some of the libraries provide extra APIs to plug in those things. But then it becomes learning the custom API of that library, undercutting the point of using Spec. And spreading out the validation steps in different places.
These kinds of libraries create a DSL for validations so that you can specify
:required for example. And it has code that it invokes when it sees that keyword.
The main trade-offs with this approach is learning the library's DSL (non-portable knowledge). And then figuring out how to work around it when it lacks enough expressiveness for your exact problem. These are often developed based on the problems the author encountered, and are not general enough to a wide range of uses. Once the DSL becomes general enough, then it has become a new language!
This is also explained in the Spec overview section Code is data (not vice versa).
This is what I used in F# to great effect. Validations are expressed in code and value transformations chain forward into the next one. Since it is code you can do most anything. It mainly works as a concise option because F# is curried by default and partial application is automatic.
You see, in F# functions always expect a precise number of arguments. So if you specify less arguments, F# will automatically partially apply those arguments and return a new function which only takes the remaining arguments. That allows you to tersely compose functions to do everything you need for the validation. Then apply data values later.
Clojure functions can take a variable number of arguments, so automatic currying and partial application are not available. Instead, the
partial function is used to do partial application and/or
comp for function composition.
I haven't found Clojure validation libraries which take this approach. Although I believe it to be possible.
It is also fair to say that composed and partial functions are not particularly easy to read at first. It takes practice to get accustomed to them. But the knowledge is generally portable.
This path adds new semantics to the language so that you can use code but also have a desired expressiveness. This is the path that Spec chose.
The main trade-off with macros is the same one that always exists with macros. Unlike regular functions, the interaction of different macros can become unpredictable. Since macros literally transform the code. (See go-block limitations, for example.)
To put it another way, macros specialize the language toward one purpose. And that specialization may conflict with another. So this approach has to be undertaken very thoughtfully.
So that is a sampling of the different types of validation approaches that I found in and around Clojure.
Personally I prefer the composition approach without macros. I am still trying to find a way to represent this well in Clojure. If I can find that, the implementation should be small and user extensible. Let me know if I missed a library that is already taking this approach.