clojure-spec

About: http://clojure.org/about/spec Guide: http://clojure.org/guides/spec API: https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html
2020-04-03T16:22:16.041600Z

So far I've been using spec mostly for validating data structures "in-place", meaning I have an x, at the front door of my API I check if it's a valid format, and if it is I continue using x inside my API. There's another way of using spec that I don't really understand yet so I'm looking for good resources on it: I have an x, and at the front door of my API I check if it's a valid format and destructure/reformat it to the format expected internally in my API, y. y might have all the same data as x but perhaps arranged in a different nested structure or with different key names. Are there any good guides to using spec for this kind of thing? I guess this is using spec as a parser, rather than just a validator. This use case is hinted at (https://clojure.org/about/spec#_conform) but I haven't found a good set of examples yet.

alexmiller 2020-04-03T16:25:30.041900Z

That’s what conform is for

alexmiller 2020-04-03T16:25:48.042400Z

“Is this valid, and if so, why?”

alexmiller 2020-04-03T16:26:39.043400Z

With the caveat that it is NOT for “perform arbitrary transformation to other structure”

2020-04-03T16:27:05.044Z

I'm trying to understand the situations it's good for. e.g. https://juxt.pro/blog/posts/parsing-with-clojure-spec.html

alexmiller 2020-04-03T16:27:28.044900Z

If you want that, use conform + standard Clojure data transformation

2020-04-03T16:27:29.045100Z

In that example, a sequence of characters is being parsed into a map that describes the components.

2020-04-03T16:27:48.045900Z

I have one map with a nested structure that I want to transform to another map with a flat structure.

alexmiller 2020-04-03T16:27:51.046100Z

Yeah, dont do that

alexmiller 2020-04-03T16:28:11.046600Z

(That applies to the string case)

alexmiller 2020-04-03T16:28:35.047200Z

If you want to transform maps, use Clojure stuff to do that

alexmiller 2020-04-03T16:28:59.047700Z

If you want to validate, use valid?

alexmiller 2020-04-03T16:29:31.048600Z

If you want to know why something is valid wrt options and alternatives, use conform

2020-04-03T16:29:55.048900Z

It's that last part I'm looking for more content on.

2020-04-03T16:30:25.049300Z

I don't quite understand when I'm dealing with a situation that's good for conform and when I'm not.

2020-04-03T16:33:57.050100Z

I remember in one of Rich's talks he mentioned David Nolen rewriting part of the cljs parser in spec. Is that code publicly available somewhere? It sounds like a powerful use case.

alexmiller 2020-04-03T16:42:09.050800Z

Well that’s mine :)

alexmiller 2020-04-03T16:42:35.051300Z

Using it to parse the input to a complex macro is a good fit

alexmiller 2020-04-03T16:42:57.051800Z

It’s particularly good for “syntax” stuff

alexmiller 2020-04-03T16:43:26.052400Z

If your input is mostly maps and stuff, probably not as useful

2020-04-03T16:45:12.053100Z

The difference in the way my input map is structured vs how my output map is structured feels like syntax to me so I wonder where the distinction lies

2020-04-03T16:47:32.053900Z

There's the idea that a map is just a seq of kv pairs. What if the spec is an s/cat of kv-pairs instead of an s/keys?

alexmiller 2020-04-03T16:49:37.054900Z

Syntax is positional (this is kind of present in the Greek roots of the word even)

alexmiller 2020-04-03T16:49:57.055300Z

Maps are not positional

alexmiller 2020-04-03T16:50:28.056Z

Position in syntax is implicit

alexmiller 2020-04-03T16:50:52.056800Z

Spec regex ops describe that, and the conformed values tell you how it was interpreted

alexmiller 2020-04-03T16:51:26.057900Z

Must of the map specs essentially return the map if it’s valid - there was no thing to figure out and tell you about

alexmiller 2020-04-03T16:53:27.059100Z

Specing a map as a seq of kv pairs is not the same thing as maps are inherently unordered

alexmiller 2020-04-03T16:53:45.059800Z

You can’t rely on position to describe the structure of the map

2020-04-03T16:54:04.060500Z

hmm, that's a good point

alexmiller 2020-04-03T16:54:09.060900Z

The regex specs like cat will even error if you try to do this, for this reason

2020-04-03T16:56:27.062700Z

I had in mind using a big s/or to describe all the possible keys. Which brings up an interesting question, how does the positionality of syntax impact s/or? I usually reach for s/or when I have a couple different possibilities for my input, like a union type, and I'm usually frustrated that I'm pushed towards destructuring and naming the options instead of conforming to just the value.

2020-04-03T16:56:38.063100Z

(s/or :even even? :small #(< % 42)) from the docstring as an example.

2020-04-03T16:57:19.063800Z

I wish the API were (s/or even? #(< % 42)) for the use I have in mind

2020-04-03T16:57:49.064200Z

I feel like there's something I don't "get" in the reasons why s/or asks for keywords to name the options.

alexmiller 2020-04-03T16:59:09.064500Z

or gives you back a map entry of tag (key) and value

2020-04-03T17:02:17.065700Z

Sure, I get that. I'm wondering why it does that when it doesn't seem to be about processing the position of a thing. Instead it seems to be about processing the alternatives of a single thing.

alexmiller 2020-04-03T17:27:59.067Z

well the idea is the same - tell me what choice you made when there was an alternative

alexmiller 2020-04-03T17:28:39.067800Z

in practice, it is often a source of issues in deeply nested data where that's the only conformed thing so I kind of would like to have some more options in spec 2

alexmiller 2020-04-03T17:28:57.068200Z

whether that's an or variant, or a s/nonconforming, or something else

alexmiller 2020-04-03T17:29:07.068500Z

but that's down the list a bit and we haven't talked about it

2020-04-03T17:33:57.068700Z

Glad to hear the sentiment is shared.

2020-04-03T17:35:38.070200Z

I often run into situations where a map is mostly just a set of specific things so s/keys works, until we add uuids into the mix and they could be strings or uuid objects. I'm using a custom conformer to get around the s/or destructuring for that case but it would be nice to have a more general solution.

alexmiller 2020-04-03T17:39:00.070500Z

one workaround right now is to wrap those s/or's in s/nonconforming

alexmiller 2020-04-03T17:39:07.070700Z

which is not documented, but exists

2020-04-03T17:49:09.070900Z

Nice!

2020-04-03T17:49:50.071700Z

(comment
  (s/conform (s/nonconforming (s/or :x uuid? :x ::sc/uuid-string)) (UUID/randomUUID))
  (s/conform (s/nonconforming (s/or :x uuid? :x ::sc/uuid-string)) (str (UUID/randomUUID)))
  (gen/generate (s/gen (s/nonconforming (s/or :x uuid? :x ::sc/uuid-string)))))
All these work as expected. The first two produce just the value, without the keyword tag. The third generates both strings and uuids.

2020-04-03T17:51:26.072200Z

My ::uuid-string spec is still a little complicated to make the generator work but I think it could be simplified.