What Clojure Spec is and what you can do with it
pixelated-noise.com
pixelated-noise.com
In the past, this required a lot of very ugly parsing code and manual error-checking. With spec, you write specs and call s/conform. If it failed, you get a nice error, especially if you pair it with expound. If it succeeded, you get a destructured value that is really easy to pull data out of. I've done this in a half dozen different libraries and i'm pretty sure i wouldn't have even written them without spec.
Edit: oh I understand what you mean, you were thinking of skipping the s/conform part, and use core.match directly. Personally, I consider spec an excellent library to describe the shape of data, and core.match allows for better describing the actions you want to take based upon that.
For example, with spec you can define a bunch of alternatives using s/or, and then use core.match to then easily traverse the results.
It’s more a matter of separation of concerns to me. I don’t use core.match for validation or describing shapes of data.
Let's say I have something like
(s/def :thespec (s/or :foo ::foo
:bar ::bar))
I can then use it in conjunction with core.match like (match [(s/conform :thespec val)]
[:foo x] (do-something-with x)
[:bar y] (do-something-else-with y))
Which imho is an easier way to navigate these things.I started playing with spec because of the idea of automated test generation, but the reality of it is that I use it as a super-charged validation library.
I think this emphasis actually does the library a disservice in that I see new users ask questions along the lines of "Should I use s/valid? to manually check inputs to my API"? The answer to that, in my usage, is "Yes! Of course!", but many people seem to think that they are using Spec wrong if they use it for something other than instrumentation and generation.
so that "john mcallister" yields { :name "john" :lastname "mcallister" } but "john " would yield { :name "john" :lastname nil } (or :todo even)
/me goes to prolog
When writing complicated macros, Spec conforming is so useful!
Also, Malli is being funded / supported by Clojurists Together [1], which is a wonderful initiative that’s also worth a look.
[0]: https://github.com/metosin/malli
[1]: https://www.clojuriststogether.org/news/q3-2020-funding-anno...
Here's a quick list of gotchas (well, they got me, so perhaps other people will find this list useful):
* `s/valid?` does not actually tell you that the data is valid
The naming of `s/valid?` suggests that you can call it on your data and find out if the data is valid according to the spec. This isn't true. What it actually tells you is if the data, when conformed, will be valid according to the spec. If you pass the data as-is to your functions (without passing it through `s/conform`), you might find that they will be surprised at what they get.
* Conformers are likely not what you think. They are not intended for coercion and have many pitfalls (for example, multi-specs dispatch on unconformed value).
* s/merge doesn't necessarily do what you wanted if you're using conformers, only the last spec passed to merge will be used for s/conform (but you're not using conformers, right?)
* specs are checked eagerly. If you think that (s/valid? ::my-spec x) will only check ::my-spec, that is not the case. It will check any key of x found in the spec registry.
I settled on a subset of spec, because of the pitfalls.
Also note for others with regard to eagerness, this is only for maps. "When conformance is checked on a map, it does two things - checking that the required attributes are included, and checking that every registered key has a conforming value. We’ll see later where optional attributes can be useful. Also note that ALL attributes are checked via keys, not just those listed in the :req and :opt keys. Thus a bare (s/keys) is valid and will check all attributes of a map without checking which keys are required or optional."
Can you explain the point of :opt with s/keys if it will always check any registered spec in if present?
Thus if you have a map without the key, the value won't be validated. But if the key is present, then it will validate its value.
If you want to make the value of the key optional, in the spec for the value of the key, you need to add `nil?` as a valid value.
Exactly. Which is something I did not expect. I expected '(s/valid? ::my-spec x)' to tell me if x is valid according to ::my-spec, checking only those keys that ::my-spec lists in :req and :opt (if present).
For maps, you might as well think of s/valid? as ignoring the first parameter. It validates anything it can.
I admit, that's surprising. Not sure why they made it so.
If you have any conformers, s/valid? will use them before validating.
So, if you have a 'set' conformer, for example, s/valid? will tell you that the data is valid even if the value is not a set, but a vector, for example.
Your code must explicitly call 'conform', checking with s/valid? is not enough.
Conformers are meant to parse the data when there are multiple possibility of what something can validate against, the conformer will disambiguate and return a result that tells you which path was chosen.
Coercion is not supported as part of Spec, you're expected to do that seperatly either before or after validating/conforming.
Not all of the above problems are due to coercion, but the majority are.
To be clear: I'm not complaining here, I find spec to be very useful and I like it, just pointing out traps for the unwary.
You should be careful asking for this.
Both Protobufs and Cap'n Proto eventually decided that "optional" creates more grief that it saves.
It is also a bit of a religious flamewar.
Caution is advised.
Which is why I'd like spec to help me with managing it. It's not obvious, because whether certain data is required or optional depends on context. But from what I've heard, bright minds at Cognitect are thinking about it, and given their track record so far, I'm pretty confident I will like the solution.
My general feelings are:
- spec is great, you should use it, the composeability and flexibility are awesome features.
- I've never once used conformers - maybe I just don't "get it" (which if so, speaks badly of them I think since I've been heavily using spec for years), but the use cases for them seem strange to me and I feel they cause more confusion than they're worth. I wish they were separated out into more separate/optional functionality.
- It's SO MUCH more powerful than things like JSON schema, but that comes at the cost of portability - there's no way we could send our spec over the wire and have someone else in a different environment use it. But also, there's no way we could implement some of our features in a tool like JSON Schema [and have it be portable] ("Is the graph represented by this data-structure free of cycles?" "When parsed by the spark SQL query parser, is this string a syntactically valid SQL query?").
- Being able to spec the whole data input up front has saved hundreds of lines of error-checking code and allows us to give much better errors up-front to our users and devs
- Spec has a lot of really cool features for generative testing, but we rarely use them since we've implemented lots of complex specs where it's not really practical to implement a generator (i.e. "strings which are valid sql queries" or "maps of maps of maps which when loaded in a particular way meet all the other requirements and are also valid DAGs"). I feel torn about this because the test features are great, but the extreme extensibility of spec is what I love most about it. I haven't often found a scenario where I actually have a use for the generative features (either the data is so simple I don't need them, or so complex that they don't work).
How would you propose to serialize clojure spec's and use them from a python app? Port the clojure compiler to python?
There's nothing stopping spec predicates from making network calls, looping forever, etc. If I wanted to be able to call my spec from other apps I'm writing, I could package it as a library easily, but a workflow like rest call->get spec->validate data (which I've implemented many times for JSON schema for simpler things) wouldn't really be practical with spec (without at least setting some really tight restrictions on what features of spec you're allowed to use)
Again, not really a failing of spec, it's just not designed for that kind of workflow.
If communicating constraints to another environment is required they should help
SCI (Small Clojure Interpreter) is 4500LOC and if you just want a barebones clojure interpreter to carefully evaluate '(> x y), you could probably fit it in 100 LOC clj. Ok you want to use Python, so 500 LOC py. Or port SCI, 4500LOC port is a few person-months given a reference implementation.
I don't think this is a really big failing of spec - I don't know of ANY validation tools that don't have to compromise between power/extensibility/ease-and-safety-of-execution-somewhere-else. Maybe if you implemented some kind of uber-validator in purely functional prolog or something?
That's because you don't Spec your functions and macros.
A lot of people have only used Spec to validate data that enters and leaves the boundary of their application. Which is a great use of Spec, and I use Spec mostly for that as well.
But there is a whole other world where Spec was designed to validate your functions and macros as well.
That's where conformers make sense.
For macros, you can use conformers to help you with writing a macro, by using Spec to define a DSL and conform to parse it out for you. It both validates the macro DSL and makes it easier for you to parse it.
For functions, conform can be useful to assert the output is what you expect for some given input. Often times, the output might depend on what kind of input you got. Conform basically tells you the kind of input it was, so in your validation you can validate differently based on each kind conform tells you it received.
For my team, generators and parsing are basically useless with spec. We just don't use them. But describing the shape of data and instrumenting our functions, using defn-spec, to ensure that the data is correct as it flows through the system is exactly what we want and nothing I've seen in Clojure land does it like spec + Orchestra can.
I think part of this may boil down to different types of testing. We primarily use functional testing, especially for our back-end, so we're starting from HTTP and hitting each endpoint as the client would. Then we ensure that the response is correct and any effects we wanted to happen did happen. This is much closer to running production code, but we do it with full instrumentation. Being able to see an error describing exactly how the data is malformed, which function was called, and what the callstack was is such a relief in Clojure.
Call to #'com.okletsplay.back-end.challenge.lol.util/provider-url did not conform to spec.
-- Spec failed --------------------
Function arguments
(nil)
^^^
should satisfy
(->
com.okletsplay.common.transit.game.lol/region->info
keys
set)
-- Relevant specs -------
:com.okletsplay.common.transit.game.lol/region:
(clojure.core/->
com.okletsplay.common.transit.game.lol/region->info
clojure.core/keys
clojure.core/set)
-------------------------
Detected 1 errorThis has to do with the philosophy. If you want to write bug free programs, and I mean, if you care A LOT about software correctness.
The idea in that case will be that all your functions will have a set of unit tests and generative tests over them that asserts that for most possible inputs they return the correct output.
Once you know that, you know that if provided valid input, your functions are going to return valid output. Because you know your function works without any bugs.
Thus, you no longer need to validate the output, only the input. Because as I just said, you now know that any valid input will result in your code returning valid output as well. So re-validating the output would be redundant.
And this goes one further. After you've thoroughly tested each functions, now you want to test the integration of your functions together. So you'd instrument your app, and now you'd write a bunch of integration tests (some maybe even using generative testing), to make sure that all possible input from the user (or external systems if intended for machine use) will result in correct program behavior and an arrangement of functions that all call each other with valid input.
Once you've tested that, you now also know that the interaction/integration of all your functions work.
At this point you are confident that given any valid user input, your program will behave as expected in output and side-effect.
You can thus now disable instrumentation.
But before you go to prod, you need one more thing, you have to protect yourself against invalid user input, because you haven't tested that and don't know how your program would behave for it. Thus with Spec, you add explicit validation over your user input which reject at the boundary the input from the user of invalid.
You now know there things:
1. All your individual functions given valid input behave as expected in output and side-effect.
2. Your integration of those functions into a program works for all given valid user input.
3. Your program rejects all invalid user input, and will only process valid user input.
Thus you can go to prod with high confidence that everything will work without any defect.
---
Now back to orchestra. Orchestra assumes that you weren't as vigilant as I just described, and that you might have not tested each and every function, or that you only wrote a small amount of tests for them which only tested a small range of inputs. Thus it assumed because of that, probably when you go towards running functional/integ tests, you want to continue to assert the output of each function is still valid, as you anticipate those will probably create inputs to functions that your tests over that function did not test.
Now, back to Clojure. Clojure assumes that you weren't as vigilant as I just described, and that you don't have static type checking for each function, or that you don't have a fixed domain for all of your enums. Thus it is assumed because of that, probably when you go running toward testing (unit, functional, or otherwise), you want to assert the validity of all of this data.
My point in re-painting your words is that we all trade certain guarantees in correctness for ease of development, maintainability, or whatever other reasons. Developers may choose Clojure over Haskell, for example, because maintaining all of that extra vigilance is undesirable overhead. Similarly, developers may reasonably choose not to unit test every single function in the code base, but instead functionally test the public endpoints and unit test only certain systems (such as the one which validates input for the public endpoints), because maintaining all of that extra vigilance is undesirable overhead.
A good blog post about this is:
https://lexi-lambda.github.io/blog/2020/08/13/types-as-axiom...
It seems like a fairly powerful technique, although you couldn't ever rely on it with production code. After several years of tinkering I managed to get a Forth interpreter written in Clojure executing a specific input string partially evaluating down to OpenGL shader code, to hardware accelerate my friend's Stackie experiment (link to his version below).
(nth (sort [0 n 5]) 1) where sort is a merge sort also successfully compiles down to just the branches you'd hand optimize it to, which is Graal's party trick. Although they're solving the problem in a bulletproof general way, so the difficulty is incomparable.
The eventual goal is to write Clojure in Clojure without it being horrendously inefficient.
Also I definitely share your feelings about clojure massively increasing my job satisfaction and teaching me how to think about programming in a new (and better tbh) way.
Out of curiosity for the folks in the know: why is this the state of affairs? Where we have the library in alpha for a long time, and being replaced already?
It's not exactly a secret that Spec is not intended to be a type-like system. But it turns out, it's perfectly possible to use Spec as a building block to do so!
As for complexity, I think instrumentation (Spec's approach) is complex. People routinely struggle with it, in fact you must use an external lib (Orchestra) to fully enable it. It also slows down execution, sometimes disproportionally.
Whereas Schema simply has a global toggle that also can be overriden in a fine-grained manner. It's explicitly made to be fast and have a type-like use.
As for type definitions, honestly both are elegant and powerful; you can quite easily use arbitrary predicates as "types" with both, and compose those predicates. Ultimately Spec is better designed because it fosters namespaced-qualifed keywords which compose better.
For addressing the instrumentation problem, I created https://github.com/nedap/speced.def which uses Clojure's :pre system. I have used :pre for over a decade so I know it has an extreme simplicity. Spec can take :pre's usefulness to the next level.